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 Object subclass: #GtGQLQuery instanceVariableNames: 'operation input' classVariableNames: '' package: 'GToolkit4GraphQL-Client' instance and pass it to the GtGQLContext Object subclass: #GtGQLContext uses: TGtOptions instanceVariableNames: 'authentication clientBuilder graphQLUrl schema name' classVariableNames: '' package: 'GToolkit4GraphQL-Client' 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 GtGQLReport subclass: #GhGQLReport instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-GitHubAPI-GraphQL' .

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" }