GraphQL in the Open Social Drupal distribution
At Open Social we’ll be using GraphQL to make the data of community platforms available to applications outside of Open Social. This is an important first step in our plans to build an entirely new front-end. In this post I will explore the options for GraphQL in Drupal, how to get started with GraphQL in Drupal and what GraphQL looks like in the Open Social Drupal distribution.
The work that this article discusses is still ongoing at the time of writing. However, you can dive into the code in the pull request on GitHub.
Standing on the shoulders of giants
What’s great about Drupal is that the saying “there’s a module for that” was true 10 years ago and still rings true today. It’s no different when you want to use Drupal to build a GraphQL server. The module is hosted on Drupal.org so that it can easily be downloaded using composer, although actual development is done on GitHub. The documentation can be found on a separate gitbook site.
The GraphQL module for Drupal is currently available in two versions. The 3.x version which has a stable release and the 4.x version which has just seen its third beta release at the time of writing. It’s worth taking a moment to look at the differences between the two versions.
The 3.x version of the module is a plug-and-play version. When enabling it, it will easily set up a GraphQL API for you based on the entity data in your Drupal website. This makes it easy to get started and is great for experimenting. A downside is that it’ll expose your data structures as-is. This may expose more than you want to. It’ll also make your GraphQL API dependant on the choices you made for other reasons. For example, all Node types will be treated the same, even though you may be using them in very different ways and want to keep them separate in your API. The result of this set-up is that you will still experience coupling between the way your front-end requests data and the way you store it in the back-end.
The 4.x version has a different design philosophy to avoid exposing your internal data structure. Instead of basing your GraphQL schema on Drupal’s entity and field structure, it requires you to define the GraphQL schema manually. This is a bit more work and requires Drupal developer knowledge. The advantage however is that you can define the GraphQL schema in the way that makes the most sense for your application. This ensures that you can provide the best experience to your decoupled front-end developers and change your internal Drupal data structures as needed without creating issues for the applications using your API.
At Open Social we have some internal data structures that were created to easily share functionality between objects. However, those same data structures have very different purposes when facing a user or application. Therefore it makes sense for us to use the 4.x version of the GraphQL module. This will allow us to create a GraphQL API that best represents the data that Open Social has.
Adding a base GraphQL module to Open Social
A new module has been created (Social GraphQL) in Open Social that sits on top of the Drupal GraphQL module. This allows shared functionality for GraphQL support to be bundled in this single module and lets any future Open Social feature that requires GraphQL to depend on this module.
The Social GraphQL module contains the default Open Social GraphQL server (open_social_graphql
). It is also the ideal place for us to implement screens needed to perform management for API consumers. The GraphQL module itself contains the configuration screen that has been used to create the open_social_graphql
server configuration. That module also contains the GraphiQL Explorer and Voyager tools which allow you to explore and test GraphQL schemas manually.
The most important part of the new Social GraphQL module is the GraphQL schema base. Open Social uses composable schema’s. This allows each module responsible for data (e.g. Social User, Social Profile, Social Courses, etc.) to implement the schema for the data that the module manages, ensuring that the schema is automatically updated with the new features when they are enabled. The base schema in the Social GraphQL module contains some shared base types (e.g. the Connection type that helps in pagination) to ensure these are consistently implemented across Open Social.
Building the base GraphQL schema
There are only three things that you need when starting out with Drupal’s GraphQL module version 4: a server entity, a GraphQL schema (.graphqls
) file, and a schema plugin. The GraphQL module contains examples for each. Below I will explain how we’ve set this up in Open Social.
The GraphQL module ships with the ability to make a composable schema. For Open Social a new GraphQL server is defined. This provides an easy target for Open Social module developers to add their own extensions onto and allows anyone using Open Social to ignore the default set-up altogether and build their own schema, if they so desire.
The social_graphql
module also defines the base of the schema in open_social.graphqls
. This contains shared types, such as the root Query
type. As well as a scalar type for Email
and primitives for pagination.
# A simplified excerpt of the Open Social base schema.
# The full version can be found in the Open Social
# repository.
"""
The schema's entry-point for queries. This acts as the public, top-level API
from which all queries must start.
"""
type Query
# The actual GraphQL schema
schema {
query: Query
}
"""
A valid email address.
"""
scalar Email
"""
A cursor for use in pagination.
"""
scalar Cursor
"""
A node on an edge.
"""
interface Node {
uuid: ID!
}
If you’re using PHPStorm I recommend installing the js-graphql plugin to add syntax highlighting for graphqls
files.
This schema on its own doesn’t do anything yet. It only lets clients know what they are able to request and how to do it. To let the GraphQL module know how to transform a GraphQL request into a response we will need to define a Schema plugin.
The OpenSocialBaseSchema
class contains the base schema plugin for the open_social GraphQL schema. The GraphQL module turns Drupal data into valid GraphQL output using resolvers. In a Schema plugin you define resolvers for fields that you’ve previously defined in your schema.
The OpenSocialBaseSchema
plugin doesn’t provide any resolvers that load data. However, it does implement resolvers for types related to pagination as well as shared types such as FormattedText
.
Extending the GraphQL schema to provide data
With the base set of functionality implemented we don’t actually have any data that we can query yet. We change this by extending the schema in the social_user
module. This module is responsible for user related functionality in Open Social so it makes sense to colocate the user part of the GraphQL API in this module.
We begin by creating a SchemaExtension plugin UserSchemaExtension
. This class does not extend SdlSchemaPluginBase
but instead extends SdlSchemaExtensionPluginBase
. The extension plugin base will not look for a regular .graphqls
file but will instead try to load a .base.graphqls
and .extension.graphqls
file with the id of the schema extension.
The social_user_schema_extension.base.graphqls
is where we create the User
type which will contain user data. An excerpt from this type definition is provided below.
"""
An Open Social user.
"""
type User implements Node {
"""
The Universally Unique Identifier for the user.
"""
uuid: ID!
"""
The display name of the user.
The specific format of the display name could depend on permissions of the
requesting user or application.
"""
display_name: String!
"""
The e-mail of the user.
Can be null if the user has not filled in an e-mail or if the user/application
making the request is not allowed to view this user's e-mail.
"""
mail: Email
# other properties omitted for brevity...
}
As you can see the User
type implements the Node
interface. It is important to note that this interface does not represent a Drupal Node type but instead points to a Node in our data graph. The required uuid
field is implemented as well as a display name and the user’s e-mail using the previously defined Email
scalar. The actual representation of an Email
is simply a string but we perform server side validation that lets clients know that this is not simply any string.
The social_user_schema_extension.base.graphqls
file has defined the shape of the user data as well as some supporting types. However, it does not yet provide applications any means of requesting the data from our graph. For this we will have to extend the Query
type. This happens in the social_user_schema_extension.extension.graphqls
file.
extend type Query {
"""
Fetch data for a specific user.
"""
user(
"""
The uuid of the user to load.
"""
uuid: ID!
) : User
}
Here we extend the Query
type to add a user
endpoint that takes the uuid
of a user and returns a User object.
The key takeaway is that in a module that’s extending a schema using a SchemaExtension
plugin there are two files important. The [plugin_id].base.graphqls
is used to define new types that are provided by the module making the extension. The [plugin_id].extension.graphqls
file is used to extend types provided by the base schema and in other module’s .base.graphqls
files.
Resolving a GraphQL request to a response with data
We now know how we define a GraphQL schema in Drupal and how to structure these so the GraphQL modules can find them. We’re still missing a step however that actually translates the request into some data that can be sent back to the client.
This translation of request to data happens by defining resolvers for the fields in a GraphQL schema. A GraphQL request always starts with one or more fields at the root of a Query, Mutation, or Subscription. In our social_user_schema_extension.extension.graphqls
schema file we defined a user
root field that returns an object with type User
. In the social_user_schema_extension.base.graphqls
schema file we defined fields such as uuid
, display_name
and mail
on the User
type.
The definition of resolvers happens in classes such asOpenSocialSchemBase
and UserSchemaExtension
. We’ll take a look at the implementation of UserSchemaExtension
to see how it defines revolvers for the user
root field and the fields in the User
type.
<?php
namespace Drupal\social_user\Plugin\GraphQL\SchemaExtension;
use Drupal\graphql\GraphQL\ResolverBuilder;
use Drupal\graphql\GraphQL\ResolverRegistryInterface;
use Drupal\graphql\Plugin\GraphQL\SchemaExtension\SdlSchemaExtensionPluginBase;
/**
* Adds user data to the Open Social GraphQL API.
*
* @SchemaExtension(
* id = "social_user_schema_extension",
* name = "Open Social - User Schema Extension",
* description = "GraphQL schema extension for Open Social user data.",
* schema = "open_social"
* )
*/
class UserSchemaExtension extends SdlSchemaExtensionPluginBase {
/**
* {@inheritdoc}
*/
public function registerResolvers(ResolverRegistryInterface $registry) {
// Resolver definitions happen here.
}
}
The class starts with a plugin annotation of the type SchemaExtension
. The id
defined here is the what determines the names of the *.base.graphql
and *.extension.graphqls
files discussed earlier. The name
and description
are helpful to developers and users of the GraphQL module’s UI. The schema
key of the annotation references our base schema plugin which this class extends.
The resolvers themselves are defined in the registerResolvers
method. Let’s fill those in for the fields we’ve previously defined for a user. In the example below I’ve omitted the class definition and kept only the method’s signature.
public function registerResolvers(ResolverRegistryInterface $registry) {
$builder = new ResolverBuilder();
// User query.
$registry->addFieldResolver('Query', 'user',
$builder->produce('entity_load_by_uuid')
->map('type', $builder->fromValue('user'))
->map('uuid', $builder->fromArgument('uuid'))
);
// User type fields.
$registry->addFieldResolver('User', 'uuid',
$builder->produce('entity_uuid')
->map('entity', $builder->fromParent())
);
$registry->addFieldResolver('User', 'display_name',
$builder->produce('entity_label')
->map('entity', $builder->fromParent())
);
$registry->addFieldResolver('User', 'mail',
// TODO: Replace with simplified form once
// https://github.com/drupal-graphql/graphql/pull/1089 lands.
// $builder->fromPath('entity:user', 'mail.value')
$builder->produce('property_path')
->map('type', $builder->fromValue('entity:user'))
->map('path', $builder->fromValue('mail.value'))
->map('value', $builder->fromParent())
);
}
This is quite a bit of code that we’ve added for our four fields (the root users
field on the Query
type and the uuid
, display_name
, and mail
field on the User
type).
At first it may look a bit weird as there are a lot of function calls but there aren’t actually any variables being manipulated. This is because defining the resolvers requires a little bit of indirection. Instead of manipulating values directly you describe what functions or classes are used to manipulate the values and describe the inputs to those methods. This allows the GraphQL module to create a pipeline that loads and transforms data in the most efficient manner.
The first thing we do is create a new ResolverBuilder
instance. This helps us access many pre-defined data producers and map the data in the resolver pipeline to the inputs of those data producers.
The next step is to begin adding resolvers for fields to the resolver registry. This is done with the addFieldResolver
function. The function takes three arguments: the type the field is being defined on, the name of the field, and a data producer instance that knows how to map inputs to outputs.
The order of definitions doesn’t really matter here. Your entire resolver mapping will be instantiated before a request is processed. However, in the Open Social codebase I’ve tried to define types higher up in the graph (such as Query
) before more specific types (such as User
).
The first such definition for this schema extension is for the user
field on the Query
type.
$registry->addFieldResolver('Query', 'user',
$builder->produce('entity_load_by_uuid')
->map('type', $builder->fromValue('user'))
->map('uuid', $builder->fromArgument('uuid'))
);
The GraphQL type and field name a pretty straightforward. The definition of the resolver itself is a bit more complex. Looking at the definition of the ResolverBuilder may leave you puzzled so let’s take a closer look at what’s going on.
Your resolver definitions will usually start with $builder->produce
which creates an instance of a DataProducerProxy
for the data producer that you’ve specified. In the above example this is entity_load_by_uuid
which is a DataProducer plugin implemented in the EntityLoadByUuid
class of the GraphQL module.
If you want to know how to create your own data producers then you can take a look at the implementation of the class. For now we’ll only look at the plugin definition to see how we can use the data producer.
/**
* @DataProducer(
* id = "entity_load_by_uuid",
* name = @Translation("Load entity by uuid"),
* description = @Translation("Loads a single entity by uuid."),
* produces = @ContextDefinition("entity",
* label = @Translation("Entity")
* ),
* consumes = {
* "type" = @ContextDefinition("string",
* label = @Translation("Entity type")
* ),
* "uuid" = @ContextDefinition("string",
* label = @Translation("Unique identifier")
* ),
* "language" = @ContextDefinition("string",
* label = @Translation("Entity language"),
* required = FALSE
* ),
* "bundles" = @ContextDefinition("string",
* label = @Translation("Entity bundle(s)"),
* multiple = TRUE,
* required = FALSE
* ),
* "access" = @ContextDefinition("boolean",
* label = @Translation("Check access"),
* required = FALSE,
* default_value = TRUE
* ),
* "access_user" = @ContextDefinition("entity:user",
* label = @Translation("User"),
* required = FALSE,
* default_value = NULL
* ),
* "access_operation" = @ContextDefinition("string",
* label = @Translation("Operation"),
* required = FALSE,
* default_value = "view"
* )
* }
* )
*/
You can see the id
is what we referenced in our call to $builder->produce('entity_load_by_uuid')
. The produces
key shows us that it produces a value of type entity
. Finally the consumes
key shows us that the data producer can take some inputs. The type
and uuid
are the only ones that are required, the others are optional. We can see that we can filter by bundles
, access
checking is on by default, the access_user
isn’t provided (the implementation defaults to the current user of the HTTP request) and the default access_operation
is to treat this access check as if the entity is being loaded for viewing.
This plugin definition tells us that we can retrieve an entity by providing the data producer with just two inputs and get an entity as output. That is exactly what our calls with the ResolverBuilder
do. First we specify the data producer that we want and then we specify how to map data to the inputs for type
and uuid
.
The $builder->fromValue('user')
call produces a map definition that will provide the data producer with the literal value 'user'
. The $builder->fromArgument('uuid')
produces a map definition that will use the argument provided in a GraphQL request (you can see the uuid
argument defined for the user
field in our schema).
Although at this point we’ve told GraphQL how it can take a request for a user and load a user, it still doesn’t know how to send that user back to the client. For this we’ll need to add fields for the User
type (as defined in our schema) that end up mapping to primitive types (such as a string, an integer or an enum value).
We can add a resolver for the uuid
field on the User
type with the following lines of code.
$registry->addFieldResolver('User', 'uuid',
$builder->produce('entity_uuid')
->map('entity', $builder->fromParent())
);
This time we use the EntityUuid
data producer which takes an entity and calls the uuid()
method on that entity. This looks very similar to how we loaded a user, except a new value is used for the second value to our map()
call.
The $builder->fromParent()
will use the output of the data producer that was used previously in our chain of data producers. In this case that is the output of our entity_load_by_uuid
data producer.
The definitions for the display_name
and mail
fields on the User
type follow similar patterns.
The property_path
producer is used often taking a typed data type, a path to a field’s value, and a typed data value (such as an entity) to work on. This is why there’s work going on to simplify this pattern into a $builder->fromPath
call.
It may not feel very useful to implement the uuid
field resolver for the User
type which is loaded using the uuid
. However, this same resolver is used when a listing is produced of users. In a listing of users a client may request only the uuid and a user name, using the returned uuid to load more data for a detailed view of a selected user.
Wrapping up
In this blogpost we’ve gone over the options for GraphQL servers in Drupal projects, outlining why Open Social has chosen for the 4.x version of the GraphQL module. A base module is introduced in Open Social that defines our base schema and shared (re-usable) data types. Next we’ve defined a schema to fetch user data and explained how we can instruct the GraphQL module to turn a request into data.
In a future article I intend to look at the implementation of the Relay Connection specification which can make it easy for GraphQL clients to implement pagination. We will also want to start testing our schema to make sure it works how we expect. If you can’t wait for the future articles and want to see how we’ve done these things, dive into the pull request that adds GraphQL to Open Social.