From 98fffbfbbebcb7f63f01654e995290bba4223755 Mon Sep 17 00:00:00 2001 From: Aliaksandra Neviarouskaya Date: Wed, 24 Jun 2026 12:18:39 +0200 Subject: [PATCH 1/4] feat: add optional player rows to match list --- docs/generated-tool-contracts.md | 1 + docs/tool-contracts.json | 70 +++++++- .../stratz_list_player_matches.input.json | 5 + .../stratz_list_player_matches.output.json | 78 +++++++++ internal/contracts/zz_generated.contracts.go | 21 ++- internal/domain/playermatch/mapping.go | 53 +++++++ internal/domain/playermatch/service.go | 10 +- internal/domain/playermatch/service_test.go | 41 +++++ internal/graphql/generated/operations.go | 150 ++++++++++++++++++ internal/graphql/generated/operations.json | 2 +- internal/graphql/operations/player.graphql | 3 + internal/mcp/player_match.go | 3 + 12 files changed, 430 insertions(+), 7 deletions(-) diff --git a/docs/generated-tool-contracts.md b/docs/generated-tool-contracts.md index 8a60745..b851339 100644 --- a/docs/generated-tool-contracts.md +++ b/docs/generated-tool-contracts.md @@ -239,6 +239,7 @@ List normalized matches for a player using bounded filters and an authenticated | `from` | false | `dateTime` | | `game_mode_id` | false | `integer` | | `hero` | false | `heroIdentifier` | +| `include_player` | false | `boolean`; default `false` | | `include_raw` | false | `boolean`; default `false` | | `limit` | false | `integer`; default `20`; minimum `1`; maximum `100` | | `lobby_type_id` | false | `integer` | diff --git a/docs/tool-contracts.json b/docs/tool-contracts.json index 03e1882..cc93e17 100644 --- a/docs/tool-contracts.json +++ b/docs/tool-contracts.json @@ -574,6 +574,69 @@ ], "additionalProperties": false }, + "playerMatchSummary": { + "type": "object", + "properties": { + "match_id": { + "$ref": "#/$defs/matchId" + }, + "started_at": { + "$ref": "#/$defs/nullableDateTime" + }, + "duration_seconds": { + "type": ["integer", "null"], + "minimum": 0 + }, + "radiant_win": { + "type": ["boolean", "null"] + }, + "radiant_score": { + "type": ["integer", "null"], + "minimum": 0 + }, + "dire_score": { + "type": ["integer", "null"], + "minimum": 0 + }, + "game_mode_id": { + "type": ["integer", "null"] + }, + "lobby_type_id": { + "type": ["integer", "null"] + }, + "region_id": { + "type": ["integer", "null"] + }, + "league_id": { + "type": ["string", "null"] + }, + "patch_id": { + "type": ["string", "null"] + }, + "parse_status": { + "type": "string", + "enum": ["parsed", "partial", "pending", "unavailable", "unknown"] + }, + "player": { + "$ref": "#/$defs/matchPlayer" + } + }, + "required": [ + "match_id", + "started_at", + "duration_seconds", + "radiant_win", + "radiant_score", + "dire_score", + "game_mode_id", + "lobby_type_id", + "region_id", + "league_id", + "patch_id", + "parse_status" + ], + "additionalProperties": false + }, "matchAvailabilityContext": { "type": "object", "properties": { @@ -1230,6 +1293,11 @@ "type": "string", "maxLength": 64 }, + "include_player": { + "type": "boolean", + "default": false, + "description": "Include the requested player's own match row on each summary when available." + }, "limit": { "type": "integer", "minimum": 1, @@ -1271,7 +1339,7 @@ "type": "array", "maxItems": 100, "items": { - "$ref": "#/$defs/matchSummary" + "$ref": "#/$defs/playerMatchSummary" } }, "page": { diff --git a/internal/contracts/generated/schemas/stratz_list_player_matches.input.json b/internal/contracts/generated/schemas/stratz_list_player_matches.input.json index 2a43fc9..22d6432 100644 --- a/internal/contracts/generated/schemas/stratz_list_player_matches.input.json +++ b/internal/contracts/generated/schemas/stratz_list_player_matches.input.json @@ -40,6 +40,11 @@ } ] }, + "include_player": { + "default": false, + "description": "Include the requested player's own match row on each summary when available.", + "type": "boolean" + }, "include_raw": { "default": false, "type": "boolean" diff --git a/internal/contracts/generated/schemas/stratz_list_player_matches.output.json b/internal/contracts/generated/schemas/stratz_list_player_matches.output.json index b23706f..85b9d3d 100644 --- a/internal/contracts/generated/schemas/stratz_list_player_matches.output.json +++ b/internal/contracts/generated/schemas/stratz_list_player_matches.output.json @@ -282,6 +282,84 @@ "null" ] }, + "player": { + "additionalProperties": false, + "properties": { + "account_id": { + "pattern": "^[0-9]{1,10}$", + "type": [ + "string", + "null" + ] + }, + "assists": { + "minimum": 0, + "type": "integer" + }, + "deaths": { + "minimum": 0, + "type": "integer" + }, + "hero_id": { + "minimum": 1, + "type": "integer" + }, + "imp": { + "description": "STRATZ Individual Match Performance (IMP) for this player when upstream provides it. Null means unavailable for the match/player.", + "type": [ + "number", + "null" + ] + }, + "kills": { + "minimum": 0, + "type": "integer" + }, + "level": { + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "networth": { + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "position": { + "maximum": 9, + "minimum": 0, + "type": "integer" + }, + "team": { + "enum": [ + "radiant", + "dire" + ], + "type": "string" + }, + "won": { + "type": "boolean" + } + }, + "required": [ + "account_id", + "hero_id", + "team", + "position", + "kills", + "deaths", + "assists", + "networth", + "level", + "imp", + "won" + ], + "type": "object" + }, "radiant_score": { "minimum": 0, "type": [ diff --git a/internal/contracts/zz_generated.contracts.go b/internal/contracts/zz_generated.contracts.go index 6f97116..3762f6d 100644 --- a/internal/contracts/zz_generated.contracts.go +++ b/internal/contracts/zz_generated.contracts.go @@ -283,6 +283,22 @@ type Player struct { type PlayerIdentifier string +type PlayerMatchSummary struct { + DireScore *int64 `json:"dire_score"` + DurationSeconds *int64 `json:"duration_seconds"` + GameModeID *int64 `json:"game_mode_id"` + LeagueID *string `json:"league_id"` + LobbyTypeID *int64 `json:"lobby_type_id"` + MatchID MatchID `json:"match_id"` + ParseStatus string `json:"parse_status"` + PatchID *string `json:"patch_id"` + Player *MatchPlayer `json:"player,omitempty"` + RadiantScore *int64 `json:"radiant_score"` + RadiantWin *bool `json:"radiant_win"` + RegionID *int64 `json:"region_id"` + StartedAt NullableDateTime `json:"started_at"` +} + type Provenance struct { Cache struct { AgeSeconds *int64 `json:"age_seconds"` @@ -540,6 +556,7 @@ type StratzListPlayerMatchesRequest struct { From *DateTime `json:"from,omitempty"` GameModeID *int64 `json:"game_mode_id,omitempty"` Hero *HeroIdentifier `json:"hero,omitempty"` + IncludePlayer *bool `json:"include_player,omitempty"` IncludeRaw *bool `json:"include_raw,omitempty"` Limit *int64 `json:"limit,omitempty"` LobbyTypeID *int64 `json:"lobby_type_id,omitempty"` @@ -552,8 +569,8 @@ type StratzListPlayerMatchesRequest struct { } type StratzListPlayerMatchesData struct { - Items []MatchSummary `json:"items"` - Page PageInfo `json:"page"` + Items []PlayerMatchSummary `json:"items"` + Page PageInfo `json:"page"` } type StratzListPlayerMatchesResponse ToolResult[StratzListPlayerMatchesData] diff --git a/internal/domain/playermatch/mapping.go b/internal/domain/playermatch/mapping.go index db0dba3..0673b9f 100644 --- a/internal/domain/playermatch/mapping.go +++ b/internal/domain/playermatch/mapping.go @@ -72,6 +72,59 @@ func mapSummary(source *upstreamMatch) contracts.MatchSummary { } } +func mapPlayerMatchSummary(source *upstreamMatch, accountID int64, includePlayer bool) contracts.PlayerMatchSummary { + summary := mapSummary(source) + playerSummary := contracts.PlayerMatchSummary{ + MatchID: summary.MatchID, + StartedAt: summary.StartedAt, + DurationSeconds: summary.DurationSeconds, + RadiantWin: summary.RadiantWin, + RadiantScore: summary.RadiantScore, + DireScore: summary.DireScore, + GameModeID: summary.GameModeID, + LobbyTypeID: summary.LobbyTypeID, + RegionID: summary.RegionID, + LeagueID: summary.LeagueID, + PatchID: summary.PatchID, + ParseStatus: summary.ParseStatus, + } + if includePlayer { + playerSummary.Player = findMatchPlayer(source, accountID) + } + return playerSummary +} + +func findMatchPlayer(source *upstreamMatch, accountID int64) *contracts.MatchPlayer { + for _, player := range source.Players { + if player.SteamAccountID == nil || *player.SteamAccountID != accountID { + continue + } + team := "dire" + if player.IsRadiant { + team = "radiant" + } + won := false + if source.DidRadiantWin != nil { + won = *source.DidRadiantWin == player.IsRadiant + } + publicAccountID := strconv.FormatInt(accountID, 10) + return &contracts.MatchPlayer{ + AccountID: &publicAccountID, + HeroID: player.HeroID, + Team: team, + Position: normalizedPlayerSlot(player.PlayerSlot), + Kills: maxZero(player.Kills), + Deaths: maxZero(player.Deaths), + Assists: maxZero(player.Assists), + Networth: nonNegative(player.Networth), + Level: nonNegative(player.Level), + Imp: player.IMP, + Won: won, + } + } + return nil +} + var gameModeIDs = map[string]int64{ "NONE": 0, "ALL_PICK": 1, "CAPTAINS_MODE": 2, "RANDOM_DRAFT": 3, "SINGLE_DRAFT": 4, "ALL_RANDOM": 5, "INTRO": 6, "DIRETIDE": 7, diff --git a/internal/domain/playermatch/service.go b/internal/domain/playermatch/service.go index f94bab4..ed7e9c0 100644 --- a/internal/domain/playermatch/service.go +++ b/internal/domain/playermatch/service.go @@ -17,7 +17,7 @@ import ( ) const ( - playerListOperationVersion = "player-matches/v1" + playerListOperationVersion = "player-matches/v2" fullMatchAvailabilityWarning = "Fight and economy breakdowns are unavailable from the current STRATZ match playback data" ) @@ -317,6 +317,7 @@ type PlayerMatchFilters struct { Result *string MinimumDurationSeconds *int64 PatchID *string + IncludePlayer bool Limit int Cursor string } @@ -422,9 +423,9 @@ func (service *Service) ListPlayerMatchesWithBudget( if err != nil { return nil, err } - items := make([]contracts.MatchSummary, 0, len(scan.Items)) + items := make([]contracts.PlayerMatchSummary, 0, len(scan.Items)) for index := range scan.Items { - items = append(items, mapSummary(&scan.Items[index])) + items = append(items, mapPlayerMatchSummary(&scan.Items[index], int64(playerID.AccountID), filters.IncludePlayer)) } var nextCursor *string if scan.Next != nil { @@ -581,6 +582,9 @@ func filterBinding(id PlayerID, filters PlayerMatchFilters) map[string]any { if filters.PatchID != nil { result["patch_id"] = *filters.PatchID } + if filters.IncludePlayer { + result["include_player"] = true + } return result } diff --git a/internal/domain/playermatch/service_test.go b/internal/domain/playermatch/service_test.go index 23213ec..4bc2cd2 100644 --- a/internal/domain/playermatch/service_test.go +++ b/internal/domain/playermatch/service_test.go @@ -346,6 +346,47 @@ func TestListPlayerMatchesBoundedContinuation(t *testing.T) { } } +func TestListPlayerMatchesCanIncludeRequestedPlayerRow(t *testing.T) { + executor := &fixtureExecutor{execute: func(_ *stratz.RequestBudget, request stratz.Request) (*stratz.Response, error) { + if !strings.Contains(request.Query, "players") || !strings.Contains(request.Query, "imp") { + t.Fatalf("player match query is missing player row fields: %s", request.Query) + } + return response(`{"player":{"steamAccountId":1,"matches":[{ + "id":123, + "didRadiantWin":true, + "parseStatus":"parsed", + "players":[ + {"steamAccountId":2,"heroId":5,"isRadiant":false,"playerSlot":128,"kills":1,"deaths":2,"assists":3,"networth":4000,"level":10,"imp":1.5}, + {"steamAccountId":1,"heroId":7,"isRadiant":true,"playerSlot":2,"kills":11,"deaths":1,"assists":14,"networth":21000,"level":25,"imp":9.25} + ] + }]}}`), nil + }} + result, err := mustService(t, executor, 5).ListPlayerMatches(context.Background(), PlayerMatchFilters{ + PlayerID: "1", + Limit: 1, + IncludePlayer: true, + }) + if err != nil { + t.Fatal(err) + } + if len(result.Data.Items) != 1 || result.Data.Items[0].Player == nil { + t.Fatalf("missing player row: %#v", result.Data.Items) + } + player := result.Data.Items[0].Player + if player.AccountID == nil || *player.AccountID != "1" || + player.HeroID != 7 || + player.Team != "radiant" || + !player.Won || + player.Kills != 11 || + player.Deaths != 1 || + player.Assists != 14 || + player.Networth == nil || *player.Networth != 21000 || + player.Level == nil || *player.Level != 25 || + player.Imp == nil || *player.Imp != 9.25 { + t.Fatalf("player row = %#v", player) + } +} + func TestListPlayerMatchesUsesCurrentGameModeAndLobbyTypeFilters(t *testing.T) { executor := &fixtureExecutor{execute: func(_ *stratz.RequestBudget, request stratz.Request) (*stratz.Response, error) { if !strings.Contains(request.Query, "gameModeId: gameMode") || diff --git a/internal/graphql/generated/operations.go b/internal/graphql/generated/operations.go index 27302c7..56ee4a3 100644 --- a/internal/graphql/generated/operations.go +++ b/internal/graphql/generated/operations.go @@ -6596,6 +6596,12 @@ func (v *StratzListPlayerMatchesPlayerPlayerType) GetMatches() []StratzListPlaye // StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType includes the requested fields of the GraphQL type MatchType. type StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType struct { MatchSummaryFields `json:"-"` + Players []StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType `json:"players"` +} + +// GetPlayers returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType.Players, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) GetPlayers() []StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType { + return v.Players } // GetId returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType.Id, and is useful for accessing the field via an interface. @@ -6689,6 +6695,8 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) UnmarshalJSON( } type __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType struct { + Players []StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType `json:"players"` + Id int64 `json:"id"` StartDateTime *int64 `json:"startDateTime"` @@ -6727,6 +6735,7 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) MarshalJSON() func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) __premarshalJSON() (*__premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType, error) { var retval __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType + retval.Players = v.Players retval.Id = v.MatchSummaryFields.Id retval.StartDateTime = v.MatchSummaryFields.StartDateTime retval.DurationSeconds = v.MatchSummaryFields.DurationSeconds @@ -6743,6 +6752,132 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) __premarshalJS return &retval, nil } +// StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType includes the requested fields of the GraphQL type MatchPlayerType. +type StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType struct { + MatchPlayerFields `json:"-"` +} + +// GetSteamAccountId returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.SteamAccountId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetSteamAccountId() *int64 { + return v.MatchPlayerFields.SteamAccountId +} + +// GetHeroId returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.HeroId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetHeroId() int { + return v.MatchPlayerFields.HeroId +} + +// GetIsRadiant returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.IsRadiant, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetIsRadiant() bool { + return v.MatchPlayerFields.IsRadiant +} + +// GetPlayerSlot returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.PlayerSlot, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetPlayerSlot() int { + return v.MatchPlayerFields.PlayerSlot +} + +// GetKills returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Kills, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetKills() int { + return v.MatchPlayerFields.Kills +} + +// GetDeaths returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Deaths, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetDeaths() int { + return v.MatchPlayerFields.Deaths +} + +// GetAssists returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Assists, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetAssists() int { + return v.MatchPlayerFields.Assists +} + +// GetNetworth returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Networth, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetNetworth() *int { + return v.MatchPlayerFields.Networth +} + +// GetLevel returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Level, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetLevel() *int { + return v.MatchPlayerFields.Level +} + +// GetImp returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Imp, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetImp() *float64 { + return v.MatchPlayerFields.Imp +} + +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) UnmarshalJSON(b []byte) error { + + if string(b) == "null" { + return nil + } + + var firstPass struct { + *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType + graphql.NoUnmarshalJSON + } + firstPass.StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal( + b, &v.MatchPlayerFields) + if err != nil { + return err + } + return nil +} + +type __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType struct { + SteamAccountId *int64 `json:"steamAccountId"` + + HeroId int `json:"heroId"` + + IsRadiant bool `json:"isRadiant"` + + PlayerSlot int `json:"playerSlot"` + + Kills int `json:"kills"` + + Deaths int `json:"deaths"` + + Assists int `json:"assists"` + + Networth *int `json:"networth"` + + Level *int `json:"level"` + + Imp *float64 `json:"imp"` +} + +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) MarshalJSON() ([]byte, error) { + premarshaled, err := v.__premarshalJSON() + if err != nil { + return nil, err + } + return json.Marshal(premarshaled) +} + +func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) __premarshalJSON() (*__premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType, error) { + var retval __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType + + retval.SteamAccountId = v.MatchPlayerFields.SteamAccountId + retval.HeroId = v.MatchPlayerFields.HeroId + retval.IsRadiant = v.MatchPlayerFields.IsRadiant + retval.PlayerSlot = v.MatchPlayerFields.PlayerSlot + retval.Kills = v.MatchPlayerFields.Kills + retval.Deaths = v.MatchPlayerFields.Deaths + retval.Assists = v.MatchPlayerFields.Assists + retval.Networth = v.MatchPlayerFields.Networth + retval.Level = v.MatchPlayerFields.Level + retval.Imp = v.MatchPlayerFields.Imp + return &retval, nil +} + // StratzListPlayerMatchesResponse is returned by StratzListPlayerMatches on success. type StratzListPlayerMatchesResponse struct { Player *StratzListPlayerMatchesPlayerPlayerType `json:"player"` @@ -8314,6 +8449,9 @@ query StratzListPlayerMatches ($steamAccountId: Long!, $request: PlayerMatchesRe steamAccountId matches(request: $request) { ... MatchSummaryFields + players { + ... MatchPlayerFields + } } } } @@ -8332,6 +8470,18 @@ fragment MatchSummaryFields on MatchType { parsedDateTime statsDateTime } +fragment MatchPlayerFields on MatchPlayerType { + steamAccountId + heroId + isRadiant + playerSlot + kills + deaths + assists + networth + level + imp +} ` func StratzListPlayerMatches( diff --git a/internal/graphql/generated/operations.json b/internal/graphql/generated/operations.json index a14efc4..f93a0db 100644 --- a/internal/graphql/generated/operations.json +++ b/internal/graphql/generated/operations.json @@ -97,7 +97,7 @@ }, { "operationName": "StratzListPlayerMatches", - "query": "\nquery StratzListPlayerMatches ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) {\n\tplayer(steamAccountId: $steamAccountId) {\n\t\tsteamAccountId\n\t\tmatches(request: $request) {\n\t\t\t... MatchSummaryFields\n\t\t}\n\t}\n}\nfragment MatchSummaryFields on MatchType {\n\tid\n\tstartDateTime\n\tdurationSeconds\n\tdidRadiantWin\n\tradiantKills\n\tdireKills\n\tgameModeId: gameMode\n\tlobbyTypeId: lobbyType\n\tregionId\n\tleagueId\n\tgameVersionId\n\tparsedDateTime\n\tstatsDateTime\n}\n", + "query": "\nquery StratzListPlayerMatches ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) {\n\tplayer(steamAccountId: $steamAccountId) {\n\t\tsteamAccountId\n\t\tmatches(request: $request) {\n\t\t\t... MatchSummaryFields\n\t\t\tplayers {\n\t\t\t\t... MatchPlayerFields\n\t\t\t}\n\t\t}\n\t}\n}\nfragment MatchSummaryFields on MatchType {\n\tid\n\tstartDateTime\n\tdurationSeconds\n\tdidRadiantWin\n\tradiantKills\n\tdireKills\n\tgameModeId: gameMode\n\tlobbyTypeId: lobbyType\n\tregionId\n\tleagueId\n\tgameVersionId\n\tparsedDateTime\n\tstatsDateTime\n}\nfragment MatchPlayerFields on MatchPlayerType {\n\tsteamAccountId\n\theroId\n\tisRadiant\n\tplayerSlot\n\tkills\n\tdeaths\n\tassists\n\tnetworth\n\tlevel\n\timp\n}\n", "sourceLocation": "internal/graphql/operations/player.graphql" }, { diff --git a/internal/graphql/operations/player.graphql b/internal/graphql/operations/player.graphql index 2246609..cbdf525 100644 --- a/internal/graphql/operations/player.graphql +++ b/internal/graphql/operations/player.graphql @@ -36,6 +36,9 @@ query StratzListPlayerMatches( steamAccountId matches(request: $request) { ...MatchSummaryFields + players { + ...MatchPlayerFields + } } } } diff --git a/internal/mcp/player_match.go b/internal/mcp/player_match.go index 6efc455..de849de 100644 --- a/internal/mcp/player_match.go +++ b/internal/mcp/player_match.go @@ -203,6 +203,9 @@ func decodePlayerMatchFilters( if value, ok := input["cursor"].(string); ok { filters.Cursor = value } + if value, ok := input["include_player"].(bool); ok { + filters.IncludePlayer = value + } for key, destination := range map[string]**int64{ "game_mode_id": &filters.GameModeID, "lobby_type_id": &filters.LobbyTypeID, From b4a991826523a4511b2922a1431204d53ed29e75 Mon Sep 17 00:00:00 2001 From: Aliaksandra Neviarouskaya Date: Wed, 24 Jun 2026 12:19:53 +0200 Subject: [PATCH 2/4] docs: document conventional commit format --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index a5a86ba..31f0603 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ Status: Current - Treat `docs/tool-contracts.json`, `internal/graphql/operations/*.graphql`, `internal/graphql/schema/bootstrap.graphql`, and `workflows/workflows.json` as canonical sources. Do not edit generated outputs directly. - Run `make generate` after canonical-source changes, `make check-generated` to detect stale artifacts, and `make check` before handoff. +- Use conventional commit messages, such as `feat: add optional player rows to match list`. - Never commit tokens, `.env`, cache databases, introspection, fetched STRATZ schemas/constants, or `.stratz-restricted`. - Preserve JSON-RPC-only stdout and centralized secret redaction. - Keep the production STRATZ endpoint fixed; inject executors only in tests. From 811674bfaf47a14ca382a18847378558b4ffcf7d Mon Sep 17 00:00:00 2001 From: Aliaksandra Neviarouskaya Date: Wed, 24 Jun 2026 12:22:49 +0200 Subject: [PATCH 3/4] fix: make player match rows cost-aware and nullable --- docs/tool-contracts.json | 2 +- .../stratz_batch_get_matches.output.json | 5 +- .../schemas/stratz_get_match.output.json | 5 +- .../stratz_list_player_matches.output.json | 5 +- internal/contracts/zz_generated.contracts.go | 2 +- internal/domain/playermatch/mapping.go | 18 +- internal/domain/playermatch/service.go | 10 +- internal/domain/playermatch/service_test.go | 17 +- internal/graphql/generated/operations.go | 342 +++++++++++++++--- internal/graphql/generated/operations.json | 7 +- internal/graphql/operations/player.graphql | 12 + 11 files changed, 359 insertions(+), 66 deletions(-) diff --git a/docs/tool-contracts.json b/docs/tool-contracts.json index cc93e17..6efc64f 100644 --- a/docs/tool-contracts.json +++ b/docs/tool-contracts.json @@ -438,7 +438,7 @@ "description": "STRATZ Individual Match Performance (IMP) for this player when upstream provides it. Null means unavailable for the match/player." }, "won": { - "type": "boolean" + "type": ["boolean", "null"] } }, "required": [ diff --git a/internal/contracts/generated/schemas/stratz_batch_get_matches.output.json b/internal/contracts/generated/schemas/stratz_batch_get_matches.output.json index e77844c..4316fdd 100644 --- a/internal/contracts/generated/schemas/stratz_batch_get_matches.output.json +++ b/internal/contracts/generated/schemas/stratz_batch_get_matches.output.json @@ -532,7 +532,10 @@ "type": "string" }, "won": { - "type": "boolean" + "type": [ + "boolean", + "null" + ] } }, "required": [ diff --git a/internal/contracts/generated/schemas/stratz_get_match.output.json b/internal/contracts/generated/schemas/stratz_get_match.output.json index 55688d7..1df5b63 100644 --- a/internal/contracts/generated/schemas/stratz_get_match.output.json +++ b/internal/contracts/generated/schemas/stratz_get_match.output.json @@ -528,7 +528,10 @@ "type": "string" }, "won": { - "type": "boolean" + "type": [ + "boolean", + "null" + ] } }, "required": [ diff --git a/internal/contracts/generated/schemas/stratz_list_player_matches.output.json b/internal/contracts/generated/schemas/stratz_list_player_matches.output.json index 85b9d3d..4e9a00f 100644 --- a/internal/contracts/generated/schemas/stratz_list_player_matches.output.json +++ b/internal/contracts/generated/schemas/stratz_list_player_matches.output.json @@ -342,7 +342,10 @@ "type": "string" }, "won": { - "type": "boolean" + "type": [ + "boolean", + "null" + ] } }, "required": [ diff --git a/internal/contracts/zz_generated.contracts.go b/internal/contracts/zz_generated.contracts.go index 3762f6d..61217ea 100644 --- a/internal/contracts/zz_generated.contracts.go +++ b/internal/contracts/zz_generated.contracts.go @@ -236,7 +236,7 @@ type MatchPlayer struct { Networth *int64 `json:"networth"` Position int64 `json:"position"` Team string `json:"team"` - Won bool `json:"won"` + Won *bool `json:"won"` } type MatchSummary struct { diff --git a/internal/domain/playermatch/mapping.go b/internal/domain/playermatch/mapping.go index 0673b9f..bbf6bb3 100644 --- a/internal/domain/playermatch/mapping.go +++ b/internal/domain/playermatch/mapping.go @@ -103,10 +103,7 @@ func findMatchPlayer(source *upstreamMatch, accountID int64) *contracts.MatchPla if player.IsRadiant { team = "radiant" } - won := false - if source.DidRadiantWin != nil { - won = *source.DidRadiantWin == player.IsRadiant - } + won := playerWon(source.DidRadiantWin, player.IsRadiant) publicAccountID := strconv.FormatInt(accountID, 10) return &contracts.MatchPlayer{ AccountID: &publicAccountID, @@ -182,10 +179,7 @@ func mapMatch(source *upstreamMatch, detail contracts.DetailLevel) contracts.Mat if player.IsRadiant { team = "radiant" } - won := false - if source.DidRadiantWin != nil { - won = *source.DidRadiantWin == player.IsRadiant - } + won := playerWon(source.DidRadiantWin, player.IsRadiant) match.Players = append(match.Players, contracts.MatchPlayer{ AccountID: accountID, HeroID: player.HeroID, @@ -261,6 +255,14 @@ func mapTimeline(source *upstreamPlaybackData) []contracts.TimelineEvent { return mapEvents(events) } +func playerWon(didRadiantWin *bool, isRadiant bool) *bool { + if didRadiantWin == nil { + return nil + } + won := *didRadiantWin == isRadiant + return &won +} + func normalizedPlayerSlot(slot int64) int64 { if slot >= 128 && slot <= 132 { return slot - 123 diff --git a/internal/domain/playermatch/service.go b/internal/domain/playermatch/service.go index ed7e9c0..cc9ff0a 100644 --- a/internal/domain/playermatch/service.go +++ b/internal/domain/playermatch/service.go @@ -370,6 +370,7 @@ func (service *Service) ListPlayerMatchesWithBudget( } rawPages := make([]any, 0, service.maxUpstreamRequests) var rateLimits []stratz.RateLimit + query, operation := listPlayerMatchesOperation(filters.IncludePlayer) pageSize := filters.Limit if pageSize < 20 { pageSize = 20 @@ -387,7 +388,7 @@ func (service *Service) ListPlayerMatchesWithBudget( "steamAccountId": int64(playerID.AccountID), "request": nativePlayerMatchRequest(filters, pageSize, offset), } - response, executeErr := service.execute(ctx, budget, generated.StratzListPlayerMatches_Operation, "StratzListPlayerMatches", variables) + response, executeErr := service.execute(ctx, budget, query, operation, variables) if executeErr != nil { return pagination.Page[int64, upstreamMatch]{}, executeErr } @@ -476,6 +477,13 @@ func (service *Service) execute( return response, nil } +func listPlayerMatchesOperation(includePlayer bool) (string, string) { + if includePlayer { + return generated.StratzListPlayerMatchesWithPlayers_Operation, "StratzListPlayerMatchesWithPlayers" + } + return generated.StratzListPlayerMatches_Operation, "StratzListPlayerMatches" +} + func matchOperation(detail contracts.DetailLevel) (string, string) { switch detail { case contracts.DetailLevelSummary: diff --git a/internal/domain/playermatch/service_test.go b/internal/domain/playermatch/service_test.go index 4bc2cd2..ed9ff1f 100644 --- a/internal/domain/playermatch/service_test.go +++ b/internal/domain/playermatch/service_test.go @@ -359,24 +359,29 @@ func TestListPlayerMatchesCanIncludeRequestedPlayerRow(t *testing.T) { {"steamAccountId":2,"heroId":5,"isRadiant":false,"playerSlot":128,"kills":1,"deaths":2,"assists":3,"networth":4000,"level":10,"imp":1.5}, {"steamAccountId":1,"heroId":7,"isRadiant":true,"playerSlot":2,"kills":11,"deaths":1,"assists":14,"networth":21000,"level":25,"imp":9.25} ] + },{ + "id":124, + "didRadiantWin":null, + "parseStatus":"parsed", + "players":[{"steamAccountId":1,"heroId":8,"isRadiant":false,"playerSlot":128,"kills":1,"deaths":2,"assists":3}] }]}}`), nil }} result, err := mustService(t, executor, 5).ListPlayerMatches(context.Background(), PlayerMatchFilters{ PlayerID: "1", - Limit: 1, + Limit: 2, IncludePlayer: true, }) if err != nil { t.Fatal(err) } - if len(result.Data.Items) != 1 || result.Data.Items[0].Player == nil { + if len(result.Data.Items) != 2 || result.Data.Items[0].Player == nil || result.Data.Items[1].Player == nil { t.Fatalf("missing player row: %#v", result.Data.Items) } player := result.Data.Items[0].Player if player.AccountID == nil || *player.AccountID != "1" || player.HeroID != 7 || player.Team != "radiant" || - !player.Won || + player.Won == nil || !*player.Won || player.Kills != 11 || player.Deaths != 1 || player.Assists != 14 || @@ -385,6 +390,9 @@ func TestListPlayerMatchesCanIncludeRequestedPlayerRow(t *testing.T) { player.Imp == nil || *player.Imp != 9.25 { t.Fatalf("player row = %#v", player) } + if result.Data.Items[1].Player.Won != nil { + t.Fatalf("unknown result was fabricated as won=%v", *result.Data.Items[1].Player.Won) + } } func TestListPlayerMatchesUsesCurrentGameModeAndLobbyTypeFilters(t *testing.T) { @@ -393,6 +401,9 @@ func TestListPlayerMatchesUsesCurrentGameModeAndLobbyTypeFilters(t *testing.T) { !strings.Contains(request.Query, "lobbyTypeId: lobbyType") { t.Fatalf("match query does not alias current STRATZ fields: %s", request.Query) } + if strings.Contains(request.Query, "players {") { + t.Fatalf("summary-only player match query fetched player rows: %s", request.Query) + } variables := request.Variables.(map[string]any) requestInput := variables["request"].(map[string]any) if got := requestInput["gameModeIds"]; !reflect.DeepEqual(got, []int64{22}) { diff --git a/internal/graphql/generated/operations.go b/internal/graphql/generated/operations.go index 56ee4a3..3529da7 100644 --- a/internal/graphql/generated/operations.go +++ b/internal/graphql/generated/operations.go @@ -6596,12 +6596,6 @@ func (v *StratzListPlayerMatchesPlayerPlayerType) GetMatches() []StratzListPlaye // StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType includes the requested fields of the GraphQL type MatchType. type StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType struct { MatchSummaryFields `json:"-"` - Players []StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType `json:"players"` -} - -// GetPlayers returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType.Players, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) GetPlayers() []StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType { - return v.Players } // GetId returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType.Id, and is useful for accessing the field via an interface. @@ -6695,8 +6689,6 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) UnmarshalJSON( } type __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType struct { - Players []StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType `json:"players"` - Id int64 `json:"id"` StartDateTime *int64 `json:"startDateTime"` @@ -6735,6 +6727,190 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) MarshalJSON() func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) __premarshalJSON() (*__premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType, error) { var retval __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType + retval.Id = v.MatchSummaryFields.Id + retval.StartDateTime = v.MatchSummaryFields.StartDateTime + retval.DurationSeconds = v.MatchSummaryFields.DurationSeconds + retval.DidRadiantWin = v.MatchSummaryFields.DidRadiantWin + retval.RadiantKills = v.MatchSummaryFields.RadiantKills + retval.DireKills = v.MatchSummaryFields.DireKills + retval.GameModeId = v.MatchSummaryFields.GameModeId + retval.LobbyTypeId = v.MatchSummaryFields.LobbyTypeId + retval.RegionId = v.MatchSummaryFields.RegionId + retval.LeagueId = v.MatchSummaryFields.LeagueId + retval.GameVersionId = v.MatchSummaryFields.GameVersionId + retval.ParsedDateTime = v.MatchSummaryFields.ParsedDateTime + retval.StatsDateTime = v.MatchSummaryFields.StatsDateTime + return &retval, nil +} + +// StratzListPlayerMatchesResponse is returned by StratzListPlayerMatches on success. +type StratzListPlayerMatchesResponse struct { + Player *StratzListPlayerMatchesPlayerPlayerType `json:"player"` +} + +// GetPlayer returns StratzListPlayerMatchesResponse.Player, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesResponse) GetPlayer() *StratzListPlayerMatchesPlayerPlayerType { + return v.Player +} + +// StratzListPlayerMatchesWithPlayersPlayerPlayerType includes the requested fields of the GraphQL type PlayerType. +type StratzListPlayerMatchesWithPlayersPlayerPlayerType struct { + SteamAccountId int64 `json:"steamAccountId"` + Matches []StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType `json:"matches"` +} + +// GetSteamAccountId returns StratzListPlayerMatchesWithPlayersPlayerPlayerType.SteamAccountId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerType) GetSteamAccountId() int64 { + return v.SteamAccountId +} + +// GetMatches returns StratzListPlayerMatchesWithPlayersPlayerPlayerType.Matches, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerType) GetMatches() []StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType { + return v.Matches +} + +// StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType includes the requested fields of the GraphQL type MatchType. +type StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType struct { + MatchSummaryFields `json:"-"` + Players []StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType `json:"players"` +} + +// GetPlayers returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.Players, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetPlayers() []StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType { + return v.Players +} + +// GetId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.Id, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetId() int64 { + return v.MatchSummaryFields.Id +} + +// GetStartDateTime returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.StartDateTime, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetStartDateTime() *int64 { + return v.MatchSummaryFields.StartDateTime +} + +// GetDurationSeconds returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.DurationSeconds, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetDurationSeconds() *int { + return v.MatchSummaryFields.DurationSeconds +} + +// GetDidRadiantWin returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.DidRadiantWin, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetDidRadiantWin() *bool { + return v.MatchSummaryFields.DidRadiantWin +} + +// GetRadiantKills returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.RadiantKills, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetRadiantKills() *int { + return v.MatchSummaryFields.RadiantKills +} + +// GetDireKills returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.DireKills, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetDireKills() *int { + return v.MatchSummaryFields.DireKills +} + +// GetGameModeId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.GameModeId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetGameModeId() *int { + return v.MatchSummaryFields.GameModeId +} + +// GetLobbyTypeId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.LobbyTypeId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetLobbyTypeId() *int { + return v.MatchSummaryFields.LobbyTypeId +} + +// GetRegionId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.RegionId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetRegionId() *int { + return v.MatchSummaryFields.RegionId +} + +// GetLeagueId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.LeagueId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetLeagueId() *int64 { + return v.MatchSummaryFields.LeagueId +} + +// GetGameVersionId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.GameVersionId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetGameVersionId() *string { + return v.MatchSummaryFields.GameVersionId +} + +// GetParsedDateTime returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.ParsedDateTime, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetParsedDateTime() *int64 { + return v.MatchSummaryFields.ParsedDateTime +} + +// GetStatsDateTime returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType.StatsDateTime, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) GetStatsDateTime() *int64 { + return v.MatchSummaryFields.StatsDateTime +} + +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) UnmarshalJSON(b []byte) error { + + if string(b) == "null" { + return nil + } + + var firstPass struct { + *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType + graphql.NoUnmarshalJSON + } + firstPass.StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal( + b, &v.MatchSummaryFields) + if err != nil { + return err + } + return nil +} + +type __premarshalStratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType struct { + Players []StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType `json:"players"` + + Id int64 `json:"id"` + + StartDateTime *int64 `json:"startDateTime"` + + DurationSeconds *int `json:"durationSeconds"` + + DidRadiantWin *bool `json:"didRadiantWin"` + + RadiantKills *int `json:"radiantKills"` + + DireKills *int `json:"direKills"` + + GameModeId *int `json:"gameModeId"` + + LobbyTypeId *int `json:"lobbyTypeId"` + + RegionId *int `json:"regionId"` + + LeagueId *int64 `json:"leagueId"` + + GameVersionId *string `json:"gameVersionId"` + + ParsedDateTime *int64 `json:"parsedDateTime"` + + StatsDateTime *int64 `json:"statsDateTime"` +} + +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) MarshalJSON() ([]byte, error) { + premarshaled, err := v.__premarshalJSON() + if err != nil { + return nil, err + } + return json.Marshal(premarshaled) +} + +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType) __premarshalJSON() (*__premarshalStratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType, error) { + var retval __premarshalStratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchType + retval.Players = v.Players retval.Id = v.MatchSummaryFields.Id retval.StartDateTime = v.MatchSummaryFields.StartDateTime @@ -6752,72 +6928,72 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchType) __premarshalJS return &retval, nil } -// StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType includes the requested fields of the GraphQL type MatchPlayerType. -type StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType struct { +// StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType includes the requested fields of the GraphQL type MatchPlayerType. +type StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType struct { MatchPlayerFields `json:"-"` } -// GetSteamAccountId returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.SteamAccountId, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetSteamAccountId() *int64 { +// GetSteamAccountId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.SteamAccountId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetSteamAccountId() *int64 { return v.MatchPlayerFields.SteamAccountId } -// GetHeroId returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.HeroId, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetHeroId() int { +// GetHeroId returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.HeroId, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetHeroId() int { return v.MatchPlayerFields.HeroId } -// GetIsRadiant returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.IsRadiant, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetIsRadiant() bool { +// GetIsRadiant returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.IsRadiant, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetIsRadiant() bool { return v.MatchPlayerFields.IsRadiant } -// GetPlayerSlot returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.PlayerSlot, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetPlayerSlot() int { +// GetPlayerSlot returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.PlayerSlot, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetPlayerSlot() int { return v.MatchPlayerFields.PlayerSlot } -// GetKills returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Kills, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetKills() int { +// GetKills returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Kills, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetKills() int { return v.MatchPlayerFields.Kills } -// GetDeaths returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Deaths, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetDeaths() int { +// GetDeaths returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Deaths, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetDeaths() int { return v.MatchPlayerFields.Deaths } -// GetAssists returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Assists, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetAssists() int { +// GetAssists returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Assists, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetAssists() int { return v.MatchPlayerFields.Assists } -// GetNetworth returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Networth, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetNetworth() *int { +// GetNetworth returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Networth, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetNetworth() *int { return v.MatchPlayerFields.Networth } -// GetLevel returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Level, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetLevel() *int { +// GetLevel returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Level, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetLevel() *int { return v.MatchPlayerFields.Level } -// GetImp returns StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Imp, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetImp() *float64 { +// GetImp returns StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType.Imp, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) GetImp() *float64 { return v.MatchPlayerFields.Imp } -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) UnmarshalJSON(b []byte) error { +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) UnmarshalJSON(b []byte) error { if string(b) == "null" { return nil } var firstPass struct { - *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType + *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType graphql.NoUnmarshalJSON } - firstPass.StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType = v + firstPass.StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -6832,7 +7008,7 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlay return nil } -type __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType struct { +type __premarshalStratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType struct { SteamAccountId *int64 `json:"steamAccountId"` HeroId int `json:"heroId"` @@ -6854,7 +7030,7 @@ type __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersM Imp *float64 `json:"imp"` } -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) MarshalJSON() ([]byte, error) { +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) MarshalJSON() ([]byte, error) { premarshaled, err := v.__premarshalJSON() if err != nil { return nil, err @@ -6862,8 +7038,8 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlay return json.Marshal(premarshaled) } -func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) __premarshalJSON() (*__premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType, error) { - var retval __premarshalStratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType +func (v *StratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType) __premarshalJSON() (*__premarshalStratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType, error) { + var retval __premarshalStratzListPlayerMatchesWithPlayersPlayerPlayerTypeMatchesMatchTypePlayersMatchPlayerType retval.SteamAccountId = v.MatchPlayerFields.SteamAccountId retval.HeroId = v.MatchPlayerFields.HeroId @@ -6878,13 +7054,13 @@ func (v *StratzListPlayerMatchesPlayerPlayerTypeMatchesMatchTypePlayersMatchPlay return &retval, nil } -// StratzListPlayerMatchesResponse is returned by StratzListPlayerMatches on success. -type StratzListPlayerMatchesResponse struct { - Player *StratzListPlayerMatchesPlayerPlayerType `json:"player"` +// StratzListPlayerMatchesWithPlayersResponse is returned by StratzListPlayerMatchesWithPlayers on success. +type StratzListPlayerMatchesWithPlayersResponse struct { + Player *StratzListPlayerMatchesWithPlayersPlayerPlayerType `json:"player"` } -// GetPlayer returns StratzListPlayerMatchesResponse.Player, and is useful for accessing the field via an interface. -func (v *StratzListPlayerMatchesResponse) GetPlayer() *StratzListPlayerMatchesPlayerPlayerType { +// GetPlayer returns StratzListPlayerMatchesWithPlayersResponse.Player, and is useful for accessing the field via an interface. +func (v *StratzListPlayerMatchesWithPlayersResponse) GetPlayer() *StratzListPlayerMatchesWithPlayersPlayerPlayerType { return v.Player } @@ -7134,6 +7310,22 @@ func (v *__StratzListPlayerMatchesInput) GetSteamAccountId() int64 { return v.St // GetRequest returns __StratzListPlayerMatchesInput.Request, and is useful for accessing the field via an interface. func (v *__StratzListPlayerMatchesInput) GetRequest() PlayerMatchesRequestType { return v.Request } +// __StratzListPlayerMatchesWithPlayersInput is used internally by genqlient +type __StratzListPlayerMatchesWithPlayersInput struct { + SteamAccountId int64 `json:"steamAccountId"` + Request PlayerMatchesRequestType `json:"request"` +} + +// GetSteamAccountId returns __StratzListPlayerMatchesWithPlayersInput.SteamAccountId, and is useful for accessing the field via an interface. +func (v *__StratzListPlayerMatchesWithPlayersInput) GetSteamAccountId() int64 { + return v.SteamAccountId +} + +// GetRequest returns __StratzListPlayerMatchesWithPlayersInput.Request, and is useful for accessing the field via an interface. +func (v *__StratzListPlayerMatchesWithPlayersInput) GetRequest() PlayerMatchesRequestType { + return v.Request +} + // The query executed by StratzGetConstants. const StratzGetConstants_Operation = ` query StratzGetConstants { @@ -8445,6 +8637,60 @@ func StratzListLiveMatches( // The query executed by StratzListPlayerMatches. const StratzListPlayerMatches_Operation = ` query StratzListPlayerMatches ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) { + player(steamAccountId: $steamAccountId) { + steamAccountId + matches(request: $request) { + ... MatchSummaryFields + } + } +} +fragment MatchSummaryFields on MatchType { + id + startDateTime + durationSeconds + didRadiantWin + radiantKills + direKills + gameModeId: gameMode + lobbyTypeId: lobbyType + regionId + leagueId + gameVersionId + parsedDateTime + statsDateTime +} +` + +func StratzListPlayerMatches( + ctx_ context.Context, + client_ graphql.Client, + steamAccountId int64, + request PlayerMatchesRequestType, +) (data_ *StratzListPlayerMatchesResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "StratzListPlayerMatches", + Query: StratzListPlayerMatches_Operation, + Variables: &__StratzListPlayerMatchesInput{ + SteamAccountId: steamAccountId, + Request: request, + }, + } + + data_ = &StratzListPlayerMatchesResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + +// The query executed by StratzListPlayerMatchesWithPlayers. +const StratzListPlayerMatchesWithPlayers_Operation = ` +query StratzListPlayerMatchesWithPlayers ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) { player(steamAccountId: $steamAccountId) { steamAccountId matches(request: $request) { @@ -8484,22 +8730,22 @@ fragment MatchPlayerFields on MatchPlayerType { } ` -func StratzListPlayerMatches( +func StratzListPlayerMatchesWithPlayers( ctx_ context.Context, client_ graphql.Client, steamAccountId int64, request PlayerMatchesRequestType, -) (data_ *StratzListPlayerMatchesResponse, err_ error) { +) (data_ *StratzListPlayerMatchesWithPlayersResponse, err_ error) { req_ := &graphql.Request{ - OpName: "StratzListPlayerMatches", - Query: StratzListPlayerMatches_Operation, - Variables: &__StratzListPlayerMatchesInput{ + OpName: "StratzListPlayerMatchesWithPlayers", + Query: StratzListPlayerMatchesWithPlayers_Operation, + Variables: &__StratzListPlayerMatchesWithPlayersInput{ SteamAccountId: steamAccountId, Request: request, }, } - data_ = &StratzListPlayerMatchesResponse{} + data_ = &StratzListPlayerMatchesWithPlayersResponse{} resp_ := &graphql.Response{Data: data_} err_ = client_.MakeRequest( diff --git a/internal/graphql/generated/operations.json b/internal/graphql/generated/operations.json index f93a0db..bed52a5 100644 --- a/internal/graphql/generated/operations.json +++ b/internal/graphql/generated/operations.json @@ -97,7 +97,12 @@ }, { "operationName": "StratzListPlayerMatches", - "query": "\nquery StratzListPlayerMatches ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) {\n\tplayer(steamAccountId: $steamAccountId) {\n\t\tsteamAccountId\n\t\tmatches(request: $request) {\n\t\t\t... MatchSummaryFields\n\t\t\tplayers {\n\t\t\t\t... MatchPlayerFields\n\t\t\t}\n\t\t}\n\t}\n}\nfragment MatchSummaryFields on MatchType {\n\tid\n\tstartDateTime\n\tdurationSeconds\n\tdidRadiantWin\n\tradiantKills\n\tdireKills\n\tgameModeId: gameMode\n\tlobbyTypeId: lobbyType\n\tregionId\n\tleagueId\n\tgameVersionId\n\tparsedDateTime\n\tstatsDateTime\n}\nfragment MatchPlayerFields on MatchPlayerType {\n\tsteamAccountId\n\theroId\n\tisRadiant\n\tplayerSlot\n\tkills\n\tdeaths\n\tassists\n\tnetworth\n\tlevel\n\timp\n}\n", + "query": "\nquery StratzListPlayerMatches ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) {\n\tplayer(steamAccountId: $steamAccountId) {\n\t\tsteamAccountId\n\t\tmatches(request: $request) {\n\t\t\t... MatchSummaryFields\n\t\t}\n\t}\n}\nfragment MatchSummaryFields on MatchType {\n\tid\n\tstartDateTime\n\tdurationSeconds\n\tdidRadiantWin\n\tradiantKills\n\tdireKills\n\tgameModeId: gameMode\n\tlobbyTypeId: lobbyType\n\tregionId\n\tleagueId\n\tgameVersionId\n\tparsedDateTime\n\tstatsDateTime\n}\n", + "sourceLocation": "internal/graphql/operations/player.graphql" + }, + { + "operationName": "StratzListPlayerMatchesWithPlayers", + "query": "\nquery StratzListPlayerMatchesWithPlayers ($steamAccountId: Long!, $request: PlayerMatchesRequestType!) {\n\tplayer(steamAccountId: $steamAccountId) {\n\t\tsteamAccountId\n\t\tmatches(request: $request) {\n\t\t\t... MatchSummaryFields\n\t\t\tplayers {\n\t\t\t\t... MatchPlayerFields\n\t\t\t}\n\t\t}\n\t}\n}\nfragment MatchSummaryFields on MatchType {\n\tid\n\tstartDateTime\n\tdurationSeconds\n\tdidRadiantWin\n\tradiantKills\n\tdireKills\n\tgameModeId: gameMode\n\tlobbyTypeId: lobbyType\n\tregionId\n\tleagueId\n\tgameVersionId\n\tparsedDateTime\n\tstatsDateTime\n}\nfragment MatchPlayerFields on MatchPlayerType {\n\tsteamAccountId\n\theroId\n\tisRadiant\n\tplayerSlot\n\tkills\n\tdeaths\n\tassists\n\tnetworth\n\tlevel\n\timp\n}\n", "sourceLocation": "internal/graphql/operations/player.graphql" }, { diff --git a/internal/graphql/operations/player.graphql b/internal/graphql/operations/player.graphql index cbdf525..ebcefaf 100644 --- a/internal/graphql/operations/player.graphql +++ b/internal/graphql/operations/player.graphql @@ -31,6 +31,18 @@ query StratzGetPlayers($steamAccountIds: [Long!]!) { query StratzListPlayerMatches( $steamAccountId: Long! $request: PlayerMatchesRequestType! +) { + player(steamAccountId: $steamAccountId) { + steamAccountId + matches(request: $request) { + ...MatchSummaryFields + } + } +} + +query StratzListPlayerMatchesWithPlayers( + $steamAccountId: Long! + $request: PlayerMatchesRequestType! ) { player(steamAccountId: $steamAccountId) { steamAccountId From 0d014e423c08d700cca5db7eaf2faeae1aeff8e6 Mon Sep 17 00:00:00 2001 From: Aliaksandra Neviarouskaya Date: Wed, 24 Jun 2026 12:23:29 +0200 Subject: [PATCH 4/4] docs: require live tests before handoff --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 31f0603..c6f10df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Status: Current # Project guidance - Treat `docs/tool-contracts.json`, `internal/graphql/operations/*.graphql`, `internal/graphql/schema/bootstrap.graphql`, and `workflows/workflows.json` as canonical sources. Do not edit generated outputs directly. -- Run `make generate` after canonical-source changes, `make check-generated` to detect stale artifacts, and `make check` before handoff. +- Run `make generate` after canonical-source changes, `make check-generated` to detect stale artifacts, and both `make check` and `make test-live` before handoff. - Use conventional commit messages, such as `feat: add optional player rows to match list`. - Never commit tokens, `.env`, cache databases, introspection, fetched STRATZ schemas/constants, or `.stratz-restricted`. - Preserve JSON-RPC-only stdout and centralized secret redaction.