Sistemas y Tecnologías Web: Servidor

Master de II. ULL. 1er cuatrimestre


Organization ULL-MII-SYTWS-2122   Classroom ULL-MII-SYTWS-2122   Campus Virtual SYTWS   Chat Chat   Profesor Casiano

GraphQL-Simple-Client

Práctica de Grupo individual

Al aceptar la asignación se le pedirá el nombre del equipo. Deberá darle como nombre Nombre-Apellidos-aluXXX (sin acentos ni caracteres especiales). Los equipos son de un sólo miembro.

Goal

Write an express web app that shows the published GH cli extensions sorted by stars. Use The GitHub GRaphQL API to get the data.

Basic Concepts and How To

Libraries

1
2
3
4
const express = require('express');
const app = express()
const { ApolloClient, InMemoryCache, HttpLink, gql } = require('@apollo/client');
const fetch = require('node-fetch');

gql

The gql function is a tagged template literal. Tagged template literals are literals delimited with backticks (`) that call a function (called the tag function) with

  1. an array of any text segments from the literal followed by
  2. arguments with the values of any substitutions

gql returns the AST of the query. See this repl node session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> let { ApolloClient, InMemoryCache, HttpLink, gql } = require('@apollo/client');
undefined
> result = gql`
... query GetDogs {
...     dogs {
.....       id
.....   }
... }
... `
{
  kind: 'Document',
  definitions: [
    {
      kind: 'OperationDefinition',
      operation: 'query',
      name: [Object],
      variableDefinitions: [],
      directives: [],
      selectionSet: [Object]
    }
  ],
  loc: { start: 0, end: 43 }
}

Getting Familiar with the GraphQL Explorer

You can run queries on real GitHub data using the GraphQL Explorer, an integrated development environment in your browser that includes docs, syntax highlighting, and validation errors.

Visit https://docs.github.com/en/graphql/overview/explorer

Designing the search query

Let us try to design a query to obtain the published GH Cli extensions sorted by stars.

Let us go to the documentation for search in the GraphQL API. It give us s.t. like:

Type: SearchResultItemConnection!

Name Description

after (String)

Returns the elements in the list that come after the specified cursor.

before (String)

Returns the elements in the list that come before the specified cursor.

first (Int)

Returns the first n elements from the list.

last (Int)

Returns the last n elements from the list.

query (String!)

The search string to look for.

type (SearchType!)

The types of search items to search within.


We see also that result of a search has the type SearchResultItemConnection that describes the list of results that matched against our search query.

Searching in GitHub

To fill the query field we need to find out how to search for "gh-extensions".

Let us go to the GitHub docs for search syntax. We need to read:

  1. Understanding the search syntax
  2. Sorting Search Results

Exercise

Try different searches at https://github.com/search/advanced

The SearchResultItemConnection Type

We see that result of a GraphQL search has the type SearchResultItemConnection that describes the list of results that matched against our search query.

These are the fields:

Fields

Name Description

codeCount (Int!)

The number of pieces of code that matched the search query.

discussionCount (Int!)

The number of discussions that matched the search query.

edges ([SearchResultItemEdge])

A list of edges.

issueCount (Int!)

The number of issues that matched the search query.

nodes ([SearchResultItem])

A list of nodes.

pageInfo (PageInfo!)

Information to aid in pagination.

repositoryCount (Int!)

The number of repositories that matched the search query.

userCount (Int!)

The number of users that matched the search query.

wikiCount (Int!)

The number of wiki pages that matched the search query.

Connections

The result type is a SearchResultItemConnection which means is a special kind of Connection.

The concept of Connection belongs to GraphQL.

A connection is a collection of objects with metadata such as edges, pageInfo, etc.

pageInfo

The pageInfo object contains information as

Fields

NameDescription

endCursor (String)

When paginating forwards, the cursor to continue.

hasNextPage (Boolean!)

When paginating forwards, are there more items?.

hasPreviousPage (Boolean!)

When paginating backwards, are there more items?.

startCursor (String)

When paginating backwards, the cursor to continue.

hasNextPage will tell us if there are more edges available, or if we’ve reached the end of this connection.

The array of records: edges

Edges will provide us with flexibility to use our data. In the example of a search, the edges are of type SearchResultItemEdge

Fields

Name Description

cursor (String!)

A cursor for use in pagination.

node (SearchResultItem)

The item at the end of the edge.

textMatches ([TextMatch])

Text matches on the result found.

edges will help us for the pagination: Each edge has a node which is a record or a data and a cursor that is a base64 encoded string to help relay with pagination.

nodes

In our example the nodes are of the type SearchResultItem that can be almost anything since they are a union type.

unions

Unions do not define any fields, so no fields may be queried on this type without the use of type refining fragments or inline fragments1.

fragments

Fragments are the primary unit of composition in GraphQL.

Fragments allow for the reuse of common repeated selections of fields, reducing duplicated text in the document. Inline Fragments can be used directly within a selection to condition upon a type condition when querying against an interface or union2.

For example,let’s say we had a page in our app, which lets us look at the marks of two students side by side. You can imagine that such a query implies to repeat the fields at least once - one for each side of the comparison.

query compare2studentsNoFragment($id1: String!, $id2: String!) {
  
  left: student(AluXXXX: $id1) {
    Nombre
    AluXXXX
    markdown
  }
  
  right: student(AluXXXX: $id2) {
    Nombre
    AluXXXX
    markdown
  }
}

The repeated fields could be extracted into a fragment and composed by a parent fragment or query.

fragment studentInfo on Student {
  Nombre
  AluXXXX
  markdown
}

query compare2students($id1: String!, $id2: String!) {
  # fragment example
  
  left: student(AluXXXX: $id1) {
    ... studentInfo
  }
  
  right: student(AluXXXX: $id2) {
    ... studentInfo
  }
}

Aliases

If you have a sharp eye, you may have noticed that, since the result object fields match the name of the field in the query but don’t include arguments, you can’t directly query for the same field with different arguments.

That’s why you need aliases - they let you rename the result of a field to anything you want. In the above example, thw two student fields would have conflicted, but since we can alias them to different names left and right, we can get both results in one request.

Default Values

Default values can also be assigned to the variables in the query by adding the default value after the type declaration.

query getStudent($id1: String = "232566@studenti.unimore.it") {
  student(AluXXXX: $id1) {
    Nombre
    markdown
  }
}

The query

After what we have explained, we can make an attempt trying this query in the explorer:

{
  search(query: "topic:gh-extension sort:stars", type: REPOSITORY, first: 2 ) {
    repositoryCount
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      cursor
      node {
        ... on Repository {
          nameWithOwner
          description
          url
          stargazers {
            totalCount
          }
        }
      }
    }
  }
}

that give us the number of repositories corresponding to gh-extensions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "data": {
    "search": {
      "repositoryCount": 111,
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "Y3Vyc29yOjI="
      },
      "edges": [
        {
          "cursor": "Y3Vyc29yOjE=",
          "node": {
            "nameWithOwner": "mislav/gh-branch",
            "description": "GitHub CLI extension for fuzzy finding, quickly switching between and deleting branches.",
            "url": "https://github.com/mislav/gh-branch",
            "stargazers": {
              "totalCount": 117
            }
          }
        },
        {
          "cursor": "Y3Vyc29yOjI=",
          "node": {
            "nameWithOwner": "vilmibm/gh-screensaver",
            "description": "full terminal animations",
            "url": "https://github.com/vilmibm/gh-screensaver",
            "stargazers": {
              "totalCount": 63
            }
          }
        }
      ]
    }
  }
}

Now we can use the endCursor to make the next request.

Building the Apollo Client

1
2
3
4
5
6
7
8
9
const cache = new InMemoryCache();
const client = new ApolloClient({
  link: new HttpLink({
    uri: "https://api.github.com/graphql", 
    fetch, 
    headers: { 'Authorization': `Bearer ${process.env['GITHUB_TOKEN']}`, },
  }),
  cache
});

HttpLink is a terminating link that sends a GraphQL operation to a remote endpoint over HTTP.

The HttpLink constructor takes an options object that can include:

  • The uri parameter is the URL of the GraphQL endpoint to send requests to. The default value is /graphql.

  • The fetch parameter is a function to use instead of calling the Fetch API directly when sending HTTP requests to your GraphQL endpoint. The function must conform to the signature of fetch.

  • The headers parameter is an object representing headers to include in every HTTP request.

Caching

In an endpoint-based API, clients can use HTTP caching to easily avoid refetching resources, and for identifying when two resources are the same. The URL in these APIs is a globally unique identifier that the client can leverage to build a cache.

HTTP cache semantics allow for cacheable responses to be stored locally by clients, moving the cache away from the server-side and closer to the client for better performance and reduced network dependence.

To support this, HTTP makes available several caching options through the Cache-Control response header that defines if the response is cacheable and, if so, for how long. Responses may only be cached if the HTTP method is GET or HEAD and the proper Cache-Control header indicates the content is cacheable.

In GraphQL, though, there’s no URL-like primitive that provides this globally unique identifier for a given object. It’s hence a best practice for the API to expose such an identifier for clients to use.

See the section Caching in Apollo Client for details on how the Apollo Client stores the results of your GraphQL queries in a local, normalized, in-memory cache. This enables Apollo Client to respond immediately to queries for already-cached data, without sending a network request.

Pagination

See Pagination in Apollo Client

Express Routes

Now the handler for requests to the main / route is simple:

1
2
3
4
5
6
 app.get('/', function(req, res) {
  client
    .query({query: firstQuery})
    .then(queryRes => handler(res, queryRes))
    .catch(error => console.error(error))
});

We make the GraphQL query firstQuery using the query method of the ApolloClient client that it is sent to the GitHub GraphQL API endpoint. It returns a Promise. When the promise is fulfilled, we get in the variable queryRes the response of the GitHub server. We pass both our object res to elaborate the response and the queryRes object to our handler function that renders the results of the query:

1
2
3
4
5
6
7
8
9
10
const handler = (res, r) => {
  const result = r.data.search;
  const repos = result.edges;
  const repoCount = result.repositoryCount
  if (result.pageInfo.hasNextPage) {
    let lastCursor = result.pageInfo.endCursor; 
    res.render('pages/index', { repos: repos, lastCursor: lastCursor});
  } 
}

The first call to handler gets the array repos corresponding to the first page of json objects describing the matching repositories. Then it computes lastCursor, the cursor of the last item in the incoming page. Both the array repos and lastCursor are passed to the view in views/pages/index.ejs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
  <%- include('../partials/head'); %>
</head>
<body class="container">

<header>
  <%- include('../partials/header'); %>
</header>

<main>
  <div class="jumbotron">
    <h1>Most Starred GitHub Cli Extensions</h1>
   
      <ol start="<%= offset %>">
        <% repos.forEach(function(repo) { %>
          <li>
            <a href="<%= repo.node.url %>" target="_blank">
                <strong><%= repo.node.nameWithOwner %></strong>
            </a>
            <%= repo.node.description %>
          </li>
        <% }); %>
      </ol>
 
        <a href="/next/<%= lastCursor %>">
            <button type="button" class="btn btn-primary btn-sm float-right">Next extensions</button>
        </a>
 
</main>

<footer>
  <%- include('../partials/footer'); %>
</footer>

</body>
</html>       

A request to the route /next/:cursor is sent when the link button to get the next page of gh cli extensions is clicked. The parameter :cursor is filled with the value of the last cursor of the previous page.

1
2
3
4
5
6
app.get('/next/:cursor', function(req, res) {
  client
    .query({ query: subsequentQueries(req.params.cursor)})
    .then(queryRes => handler(res,queryRes))
    .catch(error => console.error(error))
});

where subsequentQueries only differs from the first query in the after: $cursor argument:

{
  search(query: "topic:gh-extension sort:stars", type: REPOSITORY, first: 2, after: $cursor) {
    repositoryCount
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      ...
    }
  }
}

After we set the app to listen for requests

1
app.listen(7000, () => console.log(`App listening on port 7000!`))

we can visit the page with our browser:

References

Footnotes

  1. https://spec.graphql.org/June2018/#sec-Unions 

  2. https://spec.graphql.org/June2018/#sec-Language.Fragments 

Comment with GitHub Utterances