Skip to content

Commit 3021749

Browse files
author
Aditya Singh
committed
Add ListTeamsIncludingProperties for batched team metadata reads
Team.Properties is excluded from generated queries with `graphql:"-"`, so a caller needing properties for many teams pays one extra request per team. Add a TeamWithProperties type that embeds Team and redeclares Properties with a graphql tag, selecting the connection inline, plus ListTeamsIncludingProperties which returns every team with its tags and properties at one request per page. Tags and memberships are completed by the existing Team.Hydrate; a team holding more than 100 properties is topped up on its own rather than charging every team an extra request. Purely additive. Team.Properties, ListTeams, and Hydrate are untouched, so existing callers execute identical code paths.
1 parent aacbb54 commit 3021749

3 files changed

Lines changed: 150 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: Feature
2+
body: Add ListTeamsIncludingProperties, which returns teams with their tags and custom
3+
properties loaded in one request per page rather than one request per team.
4+
time: 2026-08-25T16:52:16+05:30

team.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,77 @@ func (client *Client) ListTeams(variables *PayloadVariables) (*TeamConnection, e
409409
return &q.Account.Teams, nil
410410
}
411411

412+
// TeamWithProperties is a Team with its custom properties already loaded.
413+
//
414+
// Team.Properties is excluded from generated queries with `graphql:"-"`, so callers that
415+
// need properties for many teams would otherwise pay one extra request per team.
416+
// Embedding Team and redeclaring the field selects the connection inline instead.
417+
type TeamWithProperties struct {
418+
Team
419+
Properties PropertiesConnection `graphql:"properties"`
420+
}
421+
422+
// ListTeamsIncludingProperties returns every team with its tags and properties populated,
423+
// costing one request per page of teams rather than one request per team.
424+
//
425+
// The inlined properties connection is selected without pagination arguments, so the API
426+
// returns its first 100 entries; any team holding more than that is topped up on its own.
427+
func (client *Client) ListTeamsIncludingProperties(variables *PayloadVariables) ([]TeamWithProperties, error) {
428+
if variables == nil {
429+
variables = client.InitialPageVariablesPointer()
430+
}
431+
432+
teams := make([]TeamWithProperties, 0)
433+
for {
434+
var q struct {
435+
Account struct {
436+
Teams struct {
437+
Nodes []TeamWithProperties
438+
PageInfo PageInfo
439+
} `graphql:"teams(after: $after, first: $first)"`
440+
}
441+
}
442+
if err := client.Query(&q, *variables, WithName("TeamListIncludingProperties")); err != nil {
443+
return nil, err
444+
}
445+
teams = append(teams, q.Account.Teams.Nodes...)
446+
if !q.Account.Teams.PageInfo.HasNextPage {
447+
break
448+
}
449+
(*variables)["after"] = q.Account.Teams.PageInfo.End
450+
}
451+
452+
for i := range teams {
453+
// Hydrate covers the tags and memberships that came back inline; it issues no
454+
// request unless one of those connections actually spilled past its first page.
455+
if err := teams[i].Hydrate(client); err != nil {
456+
return nil, err
457+
}
458+
if err := teams[i].hydrateProperties(client); err != nil {
459+
return nil, err
460+
}
461+
}
462+
return teams, nil
463+
}
464+
465+
// hydrateProperties collects any pages of properties beyond the first that the list query
466+
// already returned. It issues no request for a team holding 100 properties or fewer.
467+
func (team *TeamWithProperties) hydrateProperties(client *Client) error {
468+
// Point the embedded Team at the inlined connection so both views agree, and so
469+
// GetProperties appends later pages onto the nodes already returned.
470+
team.Team.Properties = &team.Properties
471+
472+
if !team.Properties.PageInfo.HasNextPage {
473+
team.Properties.TotalCount = len(team.Properties.Nodes)
474+
return nil
475+
}
476+
477+
variables := client.InitialPageVariablesPointer()
478+
(*variables)["after"] = team.Properties.PageInfo.End
479+
_, err := team.GetProperties(client, variables)
480+
return err
481+
}
482+
412483
func (client *Client) ListTeamsWithManager(email string, variables *PayloadVariables) (*TeamConnection, error) {
413484
var q struct {
414485
Account struct {

team_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,3 +1024,78 @@ func TestSearchTeams(t *testing.T) {
10241024
autopilot.Equals(t, "DevOps", result[0].Name)
10251025
autopilot.Equals(t, "Own Infra & Tools.", result[0].Responsibilities)
10261026
}
1027+
1028+
// ListTeamsIncludingProperties exists so that reading properties for many teams costs one
1029+
// request per page instead of one per team. Registering a single request is what proves
1030+
// that: the harness fails on any request it was not told to expect, so a per-team
1031+
// GetProperties fan-out would surface here as an unregistered call.
1032+
func TestListTeamsIncludingProperties(t *testing.T) {
1033+
// Arrange
1034+
testRequest := autopilot.NewTestRequest(
1035+
`query TeamListIncludingProperties($after:String!$first:Int!){account{teams(after: $after, first: $first){nodes{alias,id,aliases,managedAliases,contacts{address,displayName,displayType,externalId,id,isDefault,type},htmlUrl,manager{id,email,name,contacts{address,displayName,displayType,externalId,id,isDefault,type},htmlUrl,provisionedBy,role,tags{nodes{id,key,value},{{ template "pagination_request" }}},teams{nodes{alias,id},{{ template "pagination_request" }}}},memberships{nodes{role,team{alias,id},user{id,email,name}},{{ template "pagination_request" }}},name,parentTeam{alias,id},responsibilities,tags{nodes{id,key,value},{{ template "pagination_request" }}},properties{nodes{definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value},{{ template "pagination_request" }}}},{{ template "pagination_request" }}}}}`,
1036+
`{{ template "pagination_initial_query_variables" }}`,
1037+
`{ "data": {
1038+
"account": {
1039+
"teams": {
1040+
"nodes": [
1041+
{
1042+
"alias": "devops",
1043+
"aliases": [ "devops" ],
1044+
"contacts": [],
1045+
{{ template "id1" }},
1046+
"name": "DevOps",
1047+
"responsibilities": "Own Infra & Tools.",
1048+
"tags": {
1049+
"nodes": [ {{ template "tag1" }}, {{ template "tag2" }} ],
1050+
{{ template "no_pagination_response" }}
1051+
},
1052+
"properties": {
1053+
"nodes": [ {{ template "team_properties_page_1" }} ],
1054+
{{ template "no_pagination_response" }}
1055+
}
1056+
},
1057+
{
1058+
"alias": "developers",
1059+
"aliases": [ "developers" ],
1060+
"contacts": [],
1061+
{{ template "id2" }},
1062+
"name": "Developers",
1063+
"responsibilities": null,
1064+
"tags": {
1065+
"nodes": [ {{ template "tag3" }} ],
1066+
{{ template "no_pagination_response" }}
1067+
},
1068+
"properties": {
1069+
"nodes": [],
1070+
{{ template "no_pagination_response" }}
1071+
}
1072+
}
1073+
],
1074+
{{ template "no_pagination_response" }}
1075+
}
1076+
}
1077+
}}`,
1078+
)
1079+
client := BestTestClient(t, "team/list_including_properties", testRequest)
1080+
1081+
// Act
1082+
result, err := client.ListTeamsIncludingProperties(nil)
1083+
1084+
// Assert
1085+
autopilot.Ok(t, err)
1086+
autopilot.Equals(t, 2, len(result))
1087+
1088+
autopilot.Equals(t, "devops", result[0].Alias)
1089+
autopilot.Equals(t, 2, len(result[0].Tags.Nodes))
1090+
autopilot.Equals(t, "dev", result[0].Tags.Nodes[0].Key)
1091+
autopilot.Equals(t, 1, len(result[0].Properties.Nodes))
1092+
autopilot.Equals(t, 1, result[0].Properties.TotalCount)
1093+
autopilot.Equals(t, "true", string(*result[0].Properties.Nodes[0].Value))
1094+
1095+
autopilot.Equals(t, "developers", result[1].Alias)
1096+
autopilot.Equals(t, 1, len(result[1].Tags.Nodes))
1097+
autopilot.Equals(t, 0, len(result[1].Properties.Nodes))
1098+
1099+
// The embedded Team should expose the same properties the inlined field returned.
1100+
autopilot.Equals(t, 1, len(result[0].Team.Properties.Nodes))
1101+
}

0 commit comments

Comments
 (0)