Bridging GraphQL Queries Between Relay and non-Relay Schemas
I've been lately adapting an application based on WPGraphQL, to also work with the plugin GraphQL API for WordPress. This task involves adapting all field names and parameters in the GraphQL queries, converting from WPGrapQL's to the GraphQL API for WP's schema.
For instance, while WPGraphQL uses field argument first
for paginating results, the GraphQL API for WP uses limit
, and WPGraphQL's field Post.uri
is called urlPath
in the GraphQL API for WP.
Transforming the query on the WPGraphQL endpoint to work against the GraphQL API for WP endpoint
Doing the conversion, a query that works against the WPGraphQL endpoint:
{
posts(first:5) {
uri
}
}
...must be transformed to work against the GraphQL API for WP endpoint:
{
posts(limit:5) {
uri: urlPath
}
}
The response for both queries will be the same:
{
"data": {
"posts": [
{
"uri": "/blogroll/a-tale-of-two-cities-teaser/"
},
{
"uri": "/posts/cope-with-wordpress-post-demo-containing-plenty-of-blocks/"
},
{
"uri": "/posts/a-lovely-tango/"
},
{
"uri": "/uncategorized/hello-world/"
},
{
"uri": "/markup/markup-html-tags-and-formatting/"
}
]
}
}
By updating the query in this fashion, the application accessing the results under posts.uri
will receive the expected data not only from WPGraphQL, but also from the GraphQL API for WP, or from any GraphQL server. This way, the application can swap from its intended GraphQL server to another one with little effort.
Cursor-based pagination in Relay and non-Relay schemas
So far so good. Now, WPGraphQL also uses Relay's Cursor Connections Specification to implement pagination, while the GraphQL API for WP does not. This complicates matters, because this spec introduces several additional fields to the schema, such as edges
and node
, which makes the query have a different shape.
The pagination page in graphql.org explains why cursor-based pagination requires using an edges
field. Say that you want to retrieve the first 2 items on a list, and then fetch the following 2 items. With cursor-based pagination, we can pass argument after: $friendCursor
to the field to paginate, where $friendCursor
indicates the last recorded position on the list:
{
hero {
friends(first: 2 after: $friendCursor) {
name
}
}
}
Now, the cursor information does not belong to the object, but to the connection between objects (in this case, from Hero
to Friend
). Hence, the cursor
field cannot be retrieved from the object itself, but from a new entity representing the connection, which is called an "edge", and the queried object is then also available via the connection, as a "node":
{
hero {
name
friends(first: 2, after: $friendCursor) {
edges {
node {
name
}
cursor
}
}
}
}
If not using the cursor-based pagination, the query would have this other shape:
{
hero {
name
friends(first: 2, offset: $offset) {
name
}
}
}
The cursor-based pagination has added 2 extra levels to the query: edges
and node
. Then, whereas with the first query the application accesses the Friend
data under hero.friends.edges.node
, with the second one it must be done under hero.friends
.
In order to have the same application work with both queries, the second query must be adapted, adding the additional two levels.
Bridging the queries
I implemented the following solution for the Next.js WordPress Starter, so it would also work with the GraphQL API for WordPress plugin.
Consider this query:
{
categories(first: 10000) {
edges {
node {
categoryId
description
id
name
slug
}
}
}
}
Let's first adapt the field names via aliases and replace the field arguments:
{
categories: postCategories(limit: 10000) {
categoryId: id
description
id
name
slug
}
}
As explained above, this query will not work yet in the application, because the logic retrieving results from under categories.edges.node.name
will fail.
Next step is to change the shape of the query, to match the original one. For that, I introduced a field self
to every type in the GraphQL schema, which simply returns the same object where it's applied:
type QueryRoot {
self: QueryRoot!
}
type PostCategory {
self: PostCategory!
}
Its implementation (in this case, in PHP) requires barely a few lines of code: echoing back the ID and the type of the object in the resolver:
class FieldResolver
{
public function resolveValue(TypeResolverInterface $typeResolver, object $resultItem, string $fieldName, array $fieldArgs = []): mixed
{
switch ($fieldName) {
case 'self':
return $typeResolver->getID($resultItem);
}
return null;
}
public function resolveFieldTypeResolverClass(TypeResolverInterface $typeResolver, string $fieldName): ?string
{
switch ($fieldName) {
case 'self':
return $typeResolver->getIdFieldTypeResolverClass();
}
return null;
}
}
Executing this query with self
:
{
__typename
self {
__typename
}
postCategory(id: 1) {
self {
id
__typename
}
}
}
...produces these results:
{
"data": {
"__typename": "QueryRoot",
"self": {
"__typename": "QueryRoot"
},
"postCategory": {
"self": {
"id": 1,
"__typename": "PostCategory"
}
}
}
}
Now, we can use the self
field and field aliases to recreate the shape expected by the application:
{
categories: self {
edges: postCategories(limit: 10000) {
node: self {
categoryId: id
description
id
name
slug
}
}
}
}
We need to pay attention to the cardinality of the return type of each field. That's why the first self
is applied on field categories
, which returns a single instance (RootQueryToCategoryConnection
) on the WPGraphQL schema, and not on edges
, which returns a list ([RootQueryToCategoryConnectionEdge]
).
Now, replacing WPGraphQL with the GraphQL API for WordPress and then running the starter works perfectly:
As a result, I can swap the GraphQL server feeding data into the application. Even though it was designed for WPGraphQL, with little effort it can be made to work with other GraphQL servers too.
Conclusion
Using field aliases and a field self
enables to bridge the response for any two GraphQL queries of any shape.
This solution can be considered a hack, but it is a very practical one, since it allows us to reuse an application when we are required to use a different GraphQL server.