GraphQL on GitHub: a fragment for aggregated repo data
GitHub provides the GraphQL explorer to play with GraphQL data and learn how to shape your queries. When your group of queries grows to the point of repeating objects and their fields, its time to move to fragments.
A fragment in GraphQL allows you to have:
- readability - a well-named fragment shortens queries and mutations
- reusability - reuse fragments in queries and mutations
- performance - on the client, fragments and their components are a cache layer
- type-safety - the code generator that builds your GraphQL SDK includes named fragments so you can access any deeply nested objects as fragments without the types and their guards you would need to manage
Typical places to create and use fragments to DRY up your GraphQL queries include the most common schema objects. For a GitHub GraphQL schema, those can include the User and Repository.
To get the entire list of repositories in a GitHub org, you need to compensate for the cursor/paging as well as the return results. An example query for that looks like:
query OrgReposAg(
$organization: String!
$pageSize: Int
$after: String
) {
organization(login: $organization) {
repositories(
after: $after
first: $pageSize
orderBy: { field: STARGAZERS, direction: DESC }
) {
totalCount
pageInfo {
startCursor
hasNextPage
endCursor
}
edges {
cursor
node {
...MyRepoFields
}
}
}
}
}
$organization: String!
$pageSize: Int
$after: String
) {
organization(login: $organization) {
repositories(
after: $after
first: $pageSize
orderBy: { field: STARGAZERS, direction: DESC }
) {
totalCount
pageInfo {
startCursor
hasNextPage
endCursor
}
edges {
cursor
node {
...MyRepoFields
}
}
}
}
}
# This fragment extracts each repository in the edges array
# to a named type MyRepoFields, created by codegen
fragment MyRepoFields on Repository {
repositoryName: name
id
url
descriptionHTML
updatedAt
stargazers {
totalCount
}
forks {
totalCount
}
issues(states: [OPEN]) {
totalCount
}
pullRequests(states: [OPEN]) {
totalCount
}
}
fragment MyRepoFields on Repository {
repositoryName: name
id
url
descriptionHTML
updatedAt
stargazers {
totalCount
}
forks {
totalCount
}
issues(states: [OPEN]) {
totalCount
}
pullRequests(states: [OPEN]) {
totalCount
}
}
The OrgReposAg query uses the named fragment, MyRepoFields, to extract the Repository object fields and aggregations needed such as the total stargazers, forks and open issues.
This query uses the after variable to page through the org's repo list, 100 repos at a time. The variables object intially contains:
{
"organization": "Azure-samples",
"after": null,
"pageSize": 100
}
To page through all the repos in an org, each request needs to capture the paging information via the pageInfo.endCursor, in order to use that value in the next request of where to start after.
If you need the list of repos in an org, and you weren't using a fragment, you would need to create your own TypeScript type for the repository fields. That in itself isn't a huge barrier, but you need to manage the type, separately from the generated types, and you need to manage it over the time of the project and the people that come and go from it. It becomes one more thing that has to be known and verified.
This becomes increasingly problematic as the query changes over time with the nested depths of items or the remapping of data to the final shape needed by the UI. Moving the core information of the query to a fragment allows the query to organically change without interferring with your ability to get at the core information. It also provides assurance that when you need this core information in other queries in the same schema, the same core information is returned in the same named fragment (and its cooresponding type in the SDK) in each place it is used.
If you have a different solution, let me know. @dfberry