GraphQL in Drupal

GraphQL is a rapidly maturing solution available as a web service in Drupal 8 and many production sites leverage GraphQL on Drupal. GraphQL is particularly robust as a web service due to its focus on tailored responses and readily available introspection layer.

In addition, upon installation, GraphQL provides a built-in debugging tool and user interface named GraphiQL that allows us to issue queries and inspect responses in real time, located at /graphql/explorer. In this chapter, we will be using this debugger extensively due to its ease of use. To issue a request to Drupal's GraphQL implementation, all we need to do is produce a GET request to the URL /graphql with the query parameter ?query=, followed by our query, formatted as a URL-encoded string.

In this post, we retrieve content entities through GraphQL and demonstrate some of the features through the Drupal implementation of GraphQL. The GraphQL module adds a variety of permissions that allow users of various roles to execute arbitrary queries, bypass field security, or access the GraphiQL interface, among others. These are assigned to administrators only by default.

Retrieving entities with GraphQL

Unlike other modules such as RELAXed Web Services and JSON API, the GraphQL module offers a more specific and less generic set of GraphQL fields that map to Drupal equivalents. For instance, whereas RELAXed Web Services and JSON API make no distinction between nodes and users, instead treating them as generic entities, the GraphQL module treats them separately.

Note: GraphiQL offers several convenient keyboard shortcuts to access certain features. To prettify the query you have inserted, use the keyboard shortcut Shift+Ctrl+P. To run the query, use Ctrl+Enter. To access an autocomplete dropdown when providing fields, use Ctrl+Space.

Retrieving individual entities

To retrieve an individual node entity, we can issue the following anonymous query. The nodeById field accepts two arguments: id, the identifier of the node, which should be provided as a string, and language, the language of the node, which should be provided as a LanguageId (a GraphQL module-provided type that obligates language codes in capital letters without quotation marks, e.g. EN, FR). The language argument defaults to null and is hence optional.

{
  nodeById(id: "1", language: EN) {
    title
  }
}

The query above yields the following response, as we would expect. Note that we are using content generated through Devel Generate (see Chapter 7) in this scenario.

{
  "data": {
    "nodeById": {
      "title": "At Autem Hos Nostrud Saluto Voco"
    }
  }
}

As you can see in the example above, we can drill down into the node to access the fields contained therein, such as title. While these fields map on to their Drupal equivalents, as we have seen in previous responses from core REST and JSON API (e.g. status, changed, created, etc.), the GraphQL module also makes preformatted fields available, such as entityLabel. Consider the following query.

{
  nodeById(id: "1") {
    entityLabel
    changed
    entityChanged
  }
}

This query yields the following response, as seen in Figure 14-1. As you can see, whereas changed yields a UNIX timestamp, similarly to the other APIs we have covered so far, requiring us to perform date handling on the consumer, entityChanged provides us instead with the date according to Drupal's default date formatter. This is a powerful outcome and means that we can simultaneously take advantage of raw and formatted output from Drupal at the same time.

{
  "data": {
    "nodeById": {
      "entityLabel": "At Autem Hos Nostrud Saluto Voco",
      "changed": 1536169822,
      "entityChanged": "2018-09-05T17:50:22+0000"
    }
  }
}

Note: From this point forward, most figures in this chapter are GraphiQL screenshots showing identical responses to those presented in the text.

image3_9.png

Figure 14-1. The GraphQL module allows us to designate whether we desire raw or formatted output from Drupal. In this case, entityChanged has run through Drupal's date formatter.

Retrievals of users operate much the same way. Consider the following example, a query that retrieves a user entity, whose fields adhere to the User type defined in the GraphQL module.

{
  userById(id: "2") {
    uid
    name
    mail
  }
}

This query yields the following response.

{
  "data": {
    "userById": {
      "uid": 2,
      "name": "chifrothaw",
      "mail": "[email protected]"
    }
  }
}

We can also retrieve relationships within the entity itself. Consider the following example query, which fetches a node entity along with its author. In this query, we include fields that adhere to the Node type for the first level, but because entityOwner is of type User, we must use fields from the User type definition. We are also using aliases (see Chapter 8) to improve the experience for the developer building our consumer.

{
  entity: nodeById(id: "2") {
    title: entityLabel
    created: entityCreated
    author: entityOwner {
      id: uid
      name
      email: mail
    }
  }
}

The result looks something like this, as you can see in Figure 14-2.

{
  "data": {
    "entity": {
      "title": "Aliquip Quia",
      "created": "2018-09-05T17:50:22+0000",
      "author": {
        "id": 1,
        "name": "admin",
        "email": "[email protected]"
      }
    }
  }
}

image2_12.png

Figure 14-2. In this example, we use aliases to improve the consumer developer experience in addition to including information about the user who authored this entity.

As you may have noticed, while we have certain crucial information about the entity, such as when it was created or changed, who created it, and what it is called, we lack other information such as the actual body of the content entity. This is due to the fact that while the Body field is required in nodes of type Article and Page, it is fully possible in Drupal's content modeling system to do without the Body field.

Whenever we create a new content type in Drupal, as you may recall from our study of JSON API, all content entities of that type are assigned a bundle. Within the Drupal implementation of GraphQL, there is a clear distinction between the overarching Node type, which governs all nodes irrespective of their bundle, and individual NodeArticle and NodePage types, which include bundle-specific information like the Body field.

Consider the following example. In this scenario, we are using a fragment to designate that we should only retrieve the body if the node in question is an article.

{
  entity: nodeById(id: "2") {
    title: entityLabel
    created: entityCreated
    author: entityOwner {
      id: uid
      name
      email: mail
    }
    ...body
  }
}

fragment body on NodeArticle {
  body {
    value
  }
}

Recall that we can also inline this fragment to avoid repeating the field name multiple times.

{
  entity: nodeById(id: "2") {
    title: entityLabel
    created: entityCreated
    author: entityOwner {
      id: uid
      name
      email: mail
    }
    ... on NodeArticle {
      body {
        value
      }
    }
  }
}

The result of this query is the following, as seen in Figure 14-3.

{
  "data": {
    "entity": {
      "title": "Aliquip Quia",
      "created": "2018-09-05T17:50:22+0000",
      "author": {
        "id": 1,
        "name": "admin",
        "email": "[email protected]"
      },
      "body": {
        "value": "Abico ideo ratis scisco. Accumsan dignissim ea fere in quadrum venio volutpat. Facilisis genitus ideo immitto jugis magna neque pecus quae. Ad huic in jumentum meus nutus. Blandit nutus pecus ut. Aliquip commoveo inhibeo metuo."
      }
    }
  }
}

image4_6.png

Figure 14-3. We can inline a fragment based on the values of the specific bundle that we are targeting. In this case the body will only be included in the response if the entity is an article.

Note: Drupal's implementation of GraphQL makes a variety of queries available that retrieve individual entities and are well beyond the scope of this overview, including blockContentById (custom block content), commentById (comments), contactMessageById (contact form submissions), fileById (file entities), shortcutById (shortcuts), taxonomyTermById (taxonomy terms), and nodeRevisionById (node revisions). GraphiQL's autocomplete and documentation features can help you explore what fields are available in those queries.

Retrieving entity collections

In addition to queries that retrieve individual entities by identifier, the GraphQL module also offers collection queries that can perform arbitrary operations across a range of entities, such as nodeQuery and userQuery. Consider the following example.

{
  collection: nodeQuery(limit: 20) {
    entities {
      title: entityLabel
    }
  }
}

This query yields a collection of twenty entities, as you can see in Figure 14-4.

image1_10.png

Figure 14-4. In this query, we retrieve a collection of entities but limit the response to twenty entities.

In the Drupal implementation of GraphQL, nodeQuery takes several arguments: limit, the number of entities included in the response (defaults to 10); offset, the number of entities to skip before an entity should figure in the response (defaults to 0); sort, which dictates how the entities should be sorted; filter, which provides arbitrary filters; and revisions, which dictates whether revisions should be included or not. The default value of the revisions argument is DEFAULT, which loads current revisions; ALL loads all revisions and LATEST loads only the most recent revision (all values expressed without quotation marks as they adhere to their own type definition).

To also retrieve the body of these entities, we can use the following query, which drills into the per-bundle implementations. In the following example, we only include the body for articles.

{
  collection: nodeQuery(limit: 20) {
    entities {
      title: entityLabel
      ... on NodeArticle {
        body {
          value
        }
      }
    }
  }
}

As of now, there is no way in the GraphQL specification to include multiple types on a single fragment. This means that to include the body for page entities as well, we must create another fragment referring to NodePage, as you can see in the example query below.

{
  collection: nodeQuery(limit: 20) {
    entities {
      title: entityLabel
      ... on NodeArticle {
        body {
          value
        }
      }
      ... on NodePage {
        body {
          value
        }
      }
    }
  }
}