diff --git a/football/football.go b/football/football.go index 7e6ec4a..6b9a024 100644 --- a/football/football.go +++ b/football/football.go @@ -110,7 +110,7 @@ func NewFootball(c *graphql.Client) *Football { c, "players", []string{"football"}, - ), + ).WithMaxPageSize(100), Season: graphql.NewQuery[Season, SeasonParams]( c, "season", diff --git a/football/rivals.go b/football/rivals.go index 82fe57d..51601e9 100644 --- a/football/rivals.go +++ b/football/rivals.go @@ -4,8 +4,14 @@ import "git.lehouerou.net/laurent/sorare/graphql" type Rivals struct { c *graphql.Client + + PastGames *graphql.PaginatedQuery[RivalsGame, graphql.EmptyParams] } func NewRivals(c *graphql.Client) *Rivals { - return &Rivals{c: c} + return &Rivals{ + c: c, + + PastGames: graphql.NewPaginatedQuery[RivalsGame, graphql.EmptyParams](c, "pastGamesPaginated", []string{"football", "rivals"}).WithOverrideComplexity(107), + } } diff --git a/football/rivals_game.go b/football/rivals_game.go new file mode 100644 index 0000000..949c157 --- /dev/null +++ b/football/rivals_game.go @@ -0,0 +1,32 @@ +package football + +import ( + "git.lehouerou.net/laurent/sorare/graphql" + "git.lehouerou.net/laurent/sorare/types" +) + +type RivalsDraftableObject struct { + Id graphql.Id `graphql:"id"` + CapValue float64 `graphql:"capValue"` + Player struct { + Slug string `graphql:"slug"` + ActiveClub struct { + Slug string `graphql:"slug"` + } + ActiveNationalTeam struct { + Slug string `graphql:"slug"` + } + } `graphql:"player"` + Position types.Position `graphql:"position"` +} + +type RivalsGame struct { + Id graphql.Id `graphql:"id"` + Slug string `graphql:"slug"` + Cap int `graphql:"cap"` + DraftablePlayers []RivalsDraftableObject `graphql:"draftablePlayers"` + FormationKnown bool `graphql:"formationKnown"` + Game struct { + Id graphql.Id `graphql:"id"` + } `graphql:"game"` +} diff --git a/football/so5.go b/football/so5.go index 8e89393..8fa78cd 100644 --- a/football/so5.go +++ b/football/so5.go @@ -22,28 +22,28 @@ func NewSo5(c *graphql.Client) *So5 { So5Fixture: graphql.NewQuery[So5Fixture, So5FixtureParams]( c, "so5Fixture", - []string{"football", "so5"}, + []string{"so5"}, ), So5Fixtures: graphql.NewPaginatedQuery[So5Fixture, So5FixturesParams]( c, "so5Fixtures", - []string{"football", "so5"}, + []string{"so5"}, ), So5Score: graphql.NewQuery[So5Score, graphql.IdParams]( c, "so5Score", - []string{"football", "so5"}, + []string{"so5"}, ), So5Leaderboard: graphql.NewQuery[So5Leaderboard, graphql.SlugParams]( c, "so5Leaderboard", - []string{"football", "so5"}, + []string{"so5"}, ), UpcomingLeaderboards: graphql.NewQuery[[]So5Leaderboard, UpcomingLeaderboardsParams]( c, "upcomingLeaderboards", - []string{"football", "so5"}, + []string{"so5"}, ), } } diff --git a/graphql/client.go b/graphql/client.go index 4e1d366..e32d95e 100644 --- a/graphql/client.go +++ b/graphql/client.go @@ -3,6 +3,7 @@ package graphql import ( "context" "net/http" + "reflect" "sync" "time" @@ -60,12 +61,11 @@ func (c *Client) SetApiKey(apiKey string) { c.apiKey = apiKey } -func (c *Client) MaxComplexity() int { - if c.authenticated { +func (c *Client) MaxQueryComplexity() int { + if c.apiKey != "" || c.authenticated { return MaxAuthenticatedQueryComplexity - } else { - return MaxAnonymousQueryComplexity } + return MaxAnonymousQueryComplexity } func (c *Client) ConstructRawQuery( @@ -140,3 +140,52 @@ func (c *Client) Mutate( defer c.lock.Unlock() return c.gql.Mutate(ctx, q, variables, options...) } + +func GetTypeComplexity(t reflect.Type) int { + var checkStruct func(t reflect.Type, complexity *int) + checkStruct = func(t reflect.Type, complexity *int) { + if t.Kind() != reflect.Struct { + return + } + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + tag := field.Tag.Get("graphql") + if field.Type.Kind() == reflect.Struct { + *complexity++ + if len(tag) > 6 && tag[:6] == "... on" { + *complexity-- + } + checkStruct(field.Type, complexity) + continue + } + if field.Type.Kind() == reflect.Slice { + if isSimpleType(field.Type.Elem()) { + *complexity++ + } else { + *complexity++ + tmpcomplexity := 0 + checkStruct(field.Type.Elem(), &tmpcomplexity) + *complexity += tmpcomplexity * 10 + } + continue + } + if tag != "" && tag != "__typename" { + *complexity++ + } + } + } + complexity := 0 + checkStruct(t, &complexity) + return complexity +} + +func isSimpleType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String: + return true + default: + return false + } +} diff --git a/graphql/paginated_query.go b/graphql/paginated_query.go index a19ba23..2316857 100644 --- a/graphql/paginated_query.go +++ b/graphql/paginated_query.go @@ -12,6 +12,8 @@ type PaginatedQuery[ResultType any, Params any] struct { additionalPayloadParams map[string]interface{} additionalQueryParams map[string]interface{} + + overrideComplexity int } func NewPaginatedQuery[ResultType any, Params any]( @@ -25,6 +27,7 @@ func NewPaginatedQuery[ResultType any, Params any]( containerLayers: containerLayers, additionalPayloadParams: make(map[string]interface{}), additionalQueryParams: make(map[string]interface{}), + overrideComplexity: 0, } } @@ -44,6 +47,11 @@ func (pq *PaginatedQuery[ResultType, Params]) WithQueryParam( return pq } +func (pq *PaginatedQuery[ResultType, Params]) WithOverrideComplexity(complexity int) *PaginatedQuery[ResultType, Params] { + pq.overrideComplexity = complexity + return pq +} + type PaginatedQueryGetOptions struct { Limit int } @@ -68,17 +76,26 @@ func (pq *PaginatedQuery[ResultType, Params]) Get( var res []ResultType after := "" var noop ResultType - pageSize := (pq.c.MaxComplexity() - 9) / (GetComplexity(reflect.TypeOf(noop)) + 1) + complexity := pq.overrideComplexity + if complexity == 0 { + complexity = GetTypeComplexity(reflect.TypeOf(noop)) + } + maxPageSize := (pq.c.MaxQueryComplexity() - 7 - len(pq.containerLayers)) / complexity for { + pageSize := maxPageSize + if opts.Limit > 0 { + remaining := opts.Limit - len(res) + if remaining < pageSize { + pageSize = remaining + } + } page, pi, err := pq.getPage(ctx, params, after, pageSize) if err != nil { return nil, err } - for _, item := range page { - res = append(res, item) - if opts.Limit > 0 && len(res) >= opts.Limit { - return res, nil - } + res = append(res, page...) + if opts.Limit > 0 && len(res) >= opts.Limit { + return res[:opts.Limit], nil } if !pi.HasNextPage { break @@ -122,35 +139,3 @@ func (pq *PaginatedQuery[ResultType, Params]) getPage( } return res.Nodes, res.PageInfo, nil } - -func GetComplexity(t reflect.Type) int { - var checkStruct func(t reflect.Type, complexity *int) - checkStruct = func(t reflect.Type, complexity *int) { - if t.Kind() != reflect.Struct { - return - } - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - if field.Type.Kind() == reflect.Struct { - *complexity++ - tag := field.Tag.Get("graphql") - if len(tag) > 6 && tag[:6] == "... on" { - *complexity-- - } - checkStruct(field.Type, complexity) - continue - } - if field.Type.Kind() == reflect.Slice { - *complexity++ - tmpcomplexity := 0 - checkStruct(field.Type.Elem(), &tmpcomplexity) - *complexity += tmpcomplexity * 10 - continue - } - *complexity++ - } - } - complexity := 0 - checkStruct(t, &complexity) - return complexity -} diff --git a/graphql/query.go b/graphql/query.go index 63fe3e5..144c472 100644 --- a/graphql/query.go +++ b/graphql/query.go @@ -3,6 +3,7 @@ package graphql import ( "context" "fmt" + "reflect" "strings" "github.com/pkg/errors" @@ -15,6 +16,8 @@ type Query[ReturnType any, Params any] struct { additionalPayloadParams map[string]interface{} additionalQueryParams map[string]interface{} + + maxPageSize int } func NewQuery[ReturnType any, Params any]( @@ -29,6 +32,7 @@ func NewQuery[ReturnType any, Params any]( additionalPayloadParams: make(map[string]interface{}), additionalQueryParams: make(map[string]interface{}), + maxPageSize: 0, } } @@ -48,6 +52,27 @@ func (r *Query[ReturnType, Params]) WithQueryParam( return r } +func (r *Query[ReturnType, Params]) WithMaxPageSize(maxPageSize int) *Query[ReturnType, Params] { + r.maxPageSize = maxPageSize + return r +} + +func (r *Query[ReturnType, Params]) GetPageSize() int { + t := reflect.TypeOf((*ReturnType)(nil)).Elem() + if t.Kind() == reflect.Slice { + t = t.Elem() + } + complexity := GetTypeComplexity(t) + maxComplexity := r.c.MaxQueryComplexity() + layers := len(r.containerLayers) + computedPageSize := (maxComplexity - layers - 1) / complexity + + if r.maxPageSize > 0 && r.maxPageSize < computedPageSize { + return r.maxPageSize + } + return computedPageSize +} + func (r *Query[ReturnType, Params]) Get(ctx context.Context, params Params) (ReturnType, error) { paramsMap := convertParamsToMap(params)