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:

The application works well

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.