Sep 03, 2017

Drupal 8 and GraphQL. Part 1

GraphQL module for Drupal is poorly documented and the existing documentation is behind on new versions of the module. In these two articles I will share my experience with this module and what I found reading Stack overflow and issues on Github. The module is being developed and my findings might soon be outdated. Still, if at least one person finds these articles helpful, it would make me happy.

Below we will look at how we can make Drupal and GraphQL work together. Also we'll try working with a schema and custom fields. 

Drupal 8 and GraphQL. Part 1:

Drupal 8 and GraphQL. Part 2

Getting started

Let's install GraphQL module for Drupal, using Composer:

composer require drupal/graphql

We'll get this:

Here I have enabled the modules I actually used in a project.  

At the moment of writing this article it is an undocumented feature: I found it exploring issues on Github. To get fields of an Entity, we have to create View Mode for GraphQL.

Let's create a View Mode for Content and Taxonomy. We go to /admin/structure/display-modes/view and create a new View mode. We must name it GraphQL, letter case doesn't matter, though.

That's how we will be choosing fields to put into a query scheme. Sounds complicated? Let's have a look at an example with a list of company employees.

I create a taxonomy: a dictionary called Person that has 4 employees.

In our case every dictionary term has 3 additional fields: an employee's picture, their job position, and a field for internal use only, which we don't want to show up in a query from front-end. This field will show whether an employee can get free jam. This particular guy has a sweet tooth, that's why in his case the answer will be positive.

So how do we hide a field in a query? We go to the Manage Display tab and choose the display mode we created (GraphQL) in CUSTOM DISPLAY SETTINGS at the bottom. 

After applying the settings we get another display at the top. There we can control which fields appear in a query: just drag the field you want to hide to the Disabled section.  

We can also change the data format (how it is shown in a query). For example, to get a path to an image we change the format to URL to image:

After we saved the settings we can finally create our first GraphQL query!

The first query

We are ready to write our first query! But first, we must add the taxonomy we created earlier to the schema. 

The settings for the module are at /admin/config/graphql. We go to Exposed content, then mark Taxonomy Term and Person. To enable requesting of custom fields we choose the view mode we created in the ATTACH FIELDS FROM VIEW MODE column. 

After saving the settings we can finally get down to business!  We go to /graphql/explorer. On the left we will be writing our query, on the right we will see the result. Also on the right is the Docs sidebar that has all the queries available to us. There aren't many for now, but they’ll help you to get any node or term. It is enough for basic queries. We, though, want something more complicated. We'll talk about queries with parameters and output with pagination later, meanwhile let's try to request data from the taxonomy.

The schema has the following query:

taxonomyTermQuery(
    offset: Int = 0
    limit: Int = 10
    filter: TaxonomyTermQueryFilterInput
): EntityQueryResult!

Filter is the query parameters. If we go to TaxonomyTermQueryFilterInput, we'll get all parameters available to us:

tid: Int
uuid: String
langcode: String
vid: String
name: String
description: String
weight: Int
parent: Int
changed: String
defaultLangcode: Boolean

Let's create a query with the vid parameter, because we know its value: person. To see the fields we can request, we look at EntityQueryResult:

count: Int
entities: [Entity]

and Entity:

entityId: String
entityLabel: String
entityLanguage: Language
entityOwner: Entity
entityPublished: Boolean
entityCreated: String
entityChanged: String
entityBundle: String
entityUrl: Url
entityType: String
entityUuid: String
entityTranslation(language: AvailableLanguages!): Entity

Let’s request the total number of employees, the name of the employee, and their ID. We write the query on the left:

query Person {
  taxonomyTermQuery(filter: {vid: "person"}) {
    count
    entities {
      entityLabel
    }
  }
}

and get this:

Note that I redefined the names of the query and the entityLabel fields. Otherwise we would get this:

Great, we are done with our first query! But if we look at the available visible fields, we won't see the custom fields, the picture and the job position. To see them, we have to specify the type of the requested Entity.

In the Docs sidebar on the right we find Person and see the type we need:

as well as the fields we've created for the Type:

entityId: String
entityLabel: String
entityLanguage: Language
entityOwner: Entity
entityPublished: Boolean
entityCreated: String
entityChanged: String
entityBundle: String
entityUrl: Url
entityType: String
entityUuid: String
description: String
fieldPhoto: String
fieldPosition: String
name: String
entityTranslation(language: AvailableLanguages!): Entity

The field for jam is not there — exactly what we wanted!

We put this Type into the query and request the necessary fields:

Let's create a fragment from this query right away.

You can learn about fragments in GraphQL documentation

Let's rewrite the query like this:

query Person {
  staff: taxonomyTermQuery(filter: {vid: "person"}) {
    count
    persons: entities {
      ...person
    }
  }
}

fragment person on TaxonomyTermPerson {
  name: entityLabel
  photo: fieldPhoto
  position: fieldPosition
}

But how do we sort this? What if we want to see only, for example, delivery workers? To solve these issues, we will make use of Views functionality.

Using Views to extend the schema

Let's create a new View for the Person taxonomy:

Also let's add new Display for GraphQL right away:

So far so good. We are not going to make any changes. So our query will find all the existing terms for Person. 

You can change the query name in View settings:

Let's name it personList. By the way, the version of the module I used to work with didn't have this setting and the only place to change the name was here:

What's worse, the name had to be template-based, but now you can name it any way you want.

Important! Clear the Drupal cache every time after changing and saving View, otherwise these changes won't show in GraphiQL.

Now we go back to the explorer and look up person:

Here is our query! We can use the already existing fragment to rewrite the query:

query Person {
  personList {
    ...person
  }
}

That's what we get:

Let's sort the list by job position. We add new Sort Criteria to the View:

Don't forget to clear the cache. Then we go to the explorer, re-run the query, and voilà!

Now let's try to create a query with a parameter. We will filter by job position. To do so, we create a new GraphQL display in the same View and add Filter Criteria by field_position. The filter should apply only to this display: this graphql (override). Also we tick off the Expose this filter to visitors option. Then we choose the Is equal to operator to filter by job position and name the query personByPosition

The schema shows that the query got the filter parameter:

which is field_position_value: String.

Let's rewrite the query:

query Person {
  personList(filter: {field_position_value: "founder"}) {
    ...person
  }
}

Finally, we'll make a query to display output with pagination. We create another GraphQL display and name the query personByPage. In the ADVANCED group we set the following parameters:

The number of displayed elements is the default number, which can be changed in the query itself. The schema will look like this:

PersonGraphqlPersonByPageResult
arguments
page: Int = 0
pageSize: Int = 2

Here's how we write the query:

query Person($page: Int = 0, $pageSize: Int = 2) {
  personByPage(page: $page, pageSize: $pageSize) {
    count
    results {
      ...person
    }
  }
}

And now with the parameter:

So, this is how we can extend a scheme by filtering, sorting, and viewing outputs with pagination.

Custom fields in a schema

While building websites, I often had to use fields that the schema didn't have or a query that I couldn't create with View. Everything I describe below is based on fields in GraphQL module itself. I’m not sure whether it is the right approach, but I couldn't find any other options. Solutions described in the documentation or in the project's issues on Github don't actually work.

So, let's say we want to get the weight of a term. The standard schema doesn't allow for that. To extend it, we write our own graphql_custom_fields module for Drupal. The structure of subfolders:

The Fields folder will contain all our custom fields. The field description:

namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Fields;

use Drupal\taxonomy\Entity\Term;
use Drupal\graphql_core\GraphQL\FieldPluginBase;
use Youshido\GraphQL\Execution\ResolveInfo;

/**
 * GraphQL field resolving a Terms's weight.
 *
 * @GraphQLField(
 *   id = "taxonomy_term_weight",
 *   name = "weight",
 *   type = "Int",
 *   types = {"Entity"}
 * )
 */
class TaxonomyWeight extends FieldPluginBase {
  public function resolveValues($value, array $args, ResolveInfo $info) {
    if ($value instanceof Term) {
      yield intval($value->getWeight());
    } else {
      yield null;
    }
  }
}

Fields that are important for us:

To set the corresponding value for a field, we need to implement the resolveValues function. The most important variables are $value and $args.

$value — variable will be a term or the result of a query. In our case it will be a term from the Person taxonomy, which is what we request.

$args — variables are query variables (we'll talk about them later). 

Since there are many possible Entities (e. g. a Node), we check whether the value we got is a taxonomy term. If it is, then we return the weight of the term.

Let's install the module, clear the cache, and set weight for terms. Now is the moment of truth! We run the query with the new field in the fragment:

query Person {
  personList {
    ...person
  }
}

Now term weight is available!