Exploring GitHub through GraphQL
Glamorous Toolkit has built-in support for exploring remote models via GraphQL APIs.
In this tutorial, we’ll explore GitHub via its GraphQL API as an example. The plan is:
1. Get and set up GitHub authentication credentials
2. Make a model representing GitHub using GitHub’s GraphQL schema – this is the type system of objects, fields, relationships, etc. that each GraphQL API call will be validated and executed against
3. Query the model
4. Query the model with pagination
Getting started – authentication
Create a GitHub personal access token and make sure it’s a classic token since the new beta version doesn’t support GraphQL yet. Treat this key as a secret password and keep it safe. If you need more help, check out this documentation.
For ease of use, we’ll stash it in a file here:
githubAccessKeyFile := FileLocator home / '.secrets' / 'home-github.txt'. githubAccessKey := githubAccessKeyFile contents trimBoth
Create a new empty file in this spot if it’s not there.
From schema to local model
Download GitHub’s GraphQL schema and then construct a local model of it that we’ll store in a context variable:
aContext := GtGQLContext new url: 'https://api.github.com/graphql'; bearerToken: githubAccessKey; buildSchema
Note that this takes a few moments even on a fast connection and Glamorous Toolkit will be unresponsive while it’s happening.
When it’s ready, take a look at the Query view of this object to see what kind of fields a query can have and what each field’s required and non-required variables are.
Inspecting the resulting context offers a way to navigate and learn about the schema. If you get an error back, take a look at the aResponse
object to see what GitHub is telling you.
Asking questions of the GitHub model
Let’s ask the model representing GitHub a question by querying data.
Make sure the following GraphQL snippet has aContext
selected and then click the “execute” button:
query Repository($ownername: String!, $reponame: String!) { repository(owner: $ownername, name: $reponame) { url description } }
{ "ownername": "feenkcom", "reponame": "gtoolkit" }
Notice how the snippet offers completion based on the schema.
Programatically running a query
If you want to run a query from within a method, rather than as a snippet in a Lepiter page, you can create a GtGQLQuery
instance and pass it to the GtGQLContext
instance as follows:
anOperation := 'query Repository($ownername: String!, $reponame: String!) { repository(owner: $ownername, name: $reponame) { url description } }'. anInput := '{ "ownername": "feenkcom", "reponame": "gtoolkit" }'.
aContext client query: (GtGQLQuery new operation: anOperation; input: anInput).
Paginating through the repositories of an organization
Playing with an individual query is nice, but we typically want to use the query within a larger computation. One such situation is to have a widget that shows the list of some results.
A concrete example is to get all repositories of an organization. We can get an initial list with a query like this:
query Repositories($amount: Int!, $login: String!, $after: String, $privacy: RepositoryPrivacy) { organization (login: $login) { name url repositories(first: $amount, after: $after, privacy: $privacy) { edges { node { name url forks { totalCount } } } pageInfo { hasPreviousPage hasNextPage startCursor endCursor } } } }
{ "amount" : 5, "login" : "feenkcom", "after" : null, "privacy" : "PUBLIC" }
For the sake of argument, we limited the batch to 5 repositories. Of course, we want more. We want all repositories. For this, we use a Pharo snippet that makes use of a generic utility that knows how to paginate through GraphQL and that essentially behaves like a stream:
repositoriesQuery paginator cursorInputName: #after; connectorPath: #(organization repositories); itemsName: #edges"; collect: [ :each | each at: #node ] "
Inspecting the above script shows an inspector in which the repositories are loaded in batches of 5
. Scrolling through the list prompts the paginator to load more.
Paginating through repositories without specifying pageInfo
In the previous example, the query had the pageInfo specified explicitly. However, that is optional. The paginator will infer and inject the missing part. Try it:
query Repositories($count: Int!, $login: String!, $after: String, $privacy: RepositoryPrivacy) { organization (login: $login) { name url repositories(first: $count, after: $after, privacy: $privacy) { edges { node { name url forks { totalCount } } } } } }
{ "count" : 5, "login" : "feenkcom", "after" : null, "privacy" : "PUBLIC" }
Now, inspect the paginator and check the Future Query
view:
repositoriesQueryWithoutPageInfo paginator cursorInputName: #after; connectorPath: #(organization repositories); itemsName: #edges
Exploring GraphQL results through custom views
So far we played with the language integration in the environment. We can take the exploration one step further and look at how we can enhance the environment through custom views applied to the result of queries.
First, we set in the context the so called report class to be GhGQLReport
.
aContext reportClass: GhGQLReport
Now, try inspecting this:
query Organization($login: String!) { organization (login: $login) { login name url } }
{ "login": "feenkcom" }
The query does not do much by itself. It just retrieves a GitHub organization. However, the views from the inspector do more work, similar to the story from Working with a REST API: the GitHub case study.
Exploring query paths
Custom views document what is possible. These views are added incrementally, ideally directly in the inspector while exploring. However, the situation is different from an object-oriented system in which methods are associated with objects. In GraphQL, queries start from global entry points, and it is often not trivial to see which of them can advance the navigation further.
To help with finding queries, we can compute all possible paths that lead to a type. To do that, first tell the context to compute all possible query paths.
aContext buildTypePaths
Then execute a query like the one below, go to a type and then look at the Query Paths
view:
query Organization($login: String!) { organization (login: $login) { login name url } }
{ "login": "feenkcom" }