The GraphQL world functions with the help of operations like queries, mutations, and subscriptions. Anyone who has worked with GraphQL will know that query structure is generally determined in the backend, and the client is forced to follow it. With GraphQL’s auto-generated docs, this is not a big deal, especially when the developers have access to the backend.
But there can be use cases where the client might have to change the structure of queries or arguments, and this blog will explain one of such use cases and a possible solution for it.
Before getting into the details of the problem — and the solution — a basic understanding of the following tools will be helpful.
Hasura is an open-source GraphQL service that connects to user databases and microservices and auto-generates a production-ready GraphQL backend. It supports the Postgres database and its flavors (Timescale, Yugabyte). It also allows users to convert their existing database/REST endpoints into a GraphQL database.
graphql-tag
This is a JavaScript template literal tag that parses GraphQL query strings into the standard GraphQL AST. This tool lets us pass a valid query string to the backend, as it handles and auto-fixes a few common issues with query strings. Internally, this tool uses GraphQL’s parser to generate the AST
A team of three frontend and one backend developers started working on a Covid-19 project in our spare time. The backend guy was able to support us only for a very limited time. This put us in a situation where we had to decide on an easy-to-use backend so as not to rely upon the backend dev for too much support.
That’s when we decided to use Hasura as the backend. With Hasura, creating a schema and data models alone are enough, and it automatically creates the GraphQL backend with the schemas. Another beautiful feature of Hasura is that we can directly query for data from the client using where
condition and sort it using order_by
condition. This made Hasura our go-to solution.
With all the benefits of Hasura, we were already able to handle everything as a team of frontend developers and didn’t bother the backend developer much. This also helped us prototype the app quickly.
graphql-tag
Everything was perfect when we started, and it worked great for us. However, since we had to decide how to query the data on the client side, the size of the query string increased drastically, with many nested objects. This messed up the readability of the query, and it was really hard to even check whether the query string was properly structured.
A sample clumsy query will look like this:
const clumsyHasuraQuery = gql` query getProductById($id: Int!) { product( limit: 10 offset: 10 where: { id: { _eq: $id }, quantity: { _gte: 10 }, type_id: { _eq: 10, _gte: 22, _lte: 5, _in: [72,73,74] } } order_by: {category: asc, description: desc} ) { category id } } `;
The above query shows one of Hasura’s features, with which we can control and get accurate data based on the where
condition and sort it using the order_by
clause. However, this affected our readability big-time. Since it was a pet project, we visited the codebase once in a while, and we had a hard time understanding the queries.
We decided to find a solution to fix this readability issue. Our first approach was to dig into existing tooling to find a way out of it. Hasura couldn’t help much in this scenario as a backend. If we wanted to avoid this, we would have had to create custom arguments and make changes to the schema. Thus, we decided to find a solution to the client side.
In the process, we started digging into graphql-tag
before we got into Apollo Client. The graphql-tag
library was actually parsing the query string to AST. So we decided to write a wrapper over graphql-tag
, which gets the query string written in our preferred format, converts it into AST, and transforms it into the structure Hasura would expect on the backend. We named it hql-tag
, as it was specific to Hasura.
We decided to have a straight, single-word argument for where
and order_by
clauses instead of using nested objects as arguments to the query. Our intended query was supposed to look more elegant, like this:
const elegantHasuraQuery = gql` query getProductById($id: Int!) { product( limit: 10 offset: 10 # `${arguementName}_${operatorSuffix}` id_eq: $id quantity_gte: 10 type_id_eq: 10 type_id_gte: 22 type_id_lte: 5 type_id_in: [72, 73, 74] category_ord: asc description_order: desc ) { category id } } `;
hql-tag
Now that we know the structure of both the preferred and the required backend queries, it’s time to create the hql-tag
wrapper over graphql-tag
.
This wrapper is a JavaScript template literal tag, which receives the query string and passes it down to graphql-tag
. Once graphql-tag
returns the AST, hql-tag
will modify it to suit the needs of the backend.
We considered manipulating the query string directly. but it wasn’t reliable. Therefore, we decided to manipulate the AST. A sample hql-tag
implementation:
const hql = (stringArray, ...expressions) => { // Generate AST from graphql-tag const gqlAst = gql(stringArray, ...expressions); // Return the AST if its not query if (gqlAst.definitions[0].operation !== "query") return gqlAst; // Traverse through AST and modify nodes processAST(gqlAst); // Return the AST return gqlAst; };
We have our AST now, and it’s time to work on the modifications. The process goes like this.
Create AST node templates for the different type of nodes required:
const argumentTemplate = { kind: "Argument", name: { kind: "Name", value: "where", }, value: { kind: "ObjectValue", fields: [], }, };
Traverse through the nodes and process the nodes that are arguments:
processNodes(node, parent, key, index);
If it’s an argument, check whether it has any of the predefined operators as a suffix. The format of the customized argument is ${argument_name}_${operator}
.
The where
arguments should have any one of the supporting arguments, separated by an underscore _
. Here are the where
operators:
[ "eq", "gte", "gt", "ilike", "in", "like", "lt", "lte", "neq", "nilike", "nin", "nlike", "similar", "nsimilar" ]
The order_by
arguments should have a suffix of ord
or order
. The operators are:
["ord","order"] 1. ord - "short form" 2. order - "full form"
If an argument node has any of the operators, then the argument name and values are extracted and loaded into predefined templates.
- argumentTemplate.value.fields.push(...);
This constructs a new AST node, which replaces the argument node, and the end result is an AST in the format required by the server.
By modifying the AST, we customize the query on the client-side and we were able to achieve a readable and elegant GraphQL query. This hql-tag
library is independent and works with all GraphQL client frameworks.
You can find the hql-tag
library on GitHub and npm. Its usage is simple. A graphql-tag
query would look like this:
import gql from 'graphql-tag'; const clumsyHasuraQuery = gql` // ... query `;
The only change required to use the library is to modify the import statement:
import gql from 'hql-tag'; const elegantHasuraQuery = gql` // ... query `;
Now we are ready to use customized arguments in the queries.
Though this library was tailor-made for Hasura, the approach of client-side query customization by modifying AST can come in handy when dealing with a large codebase and multiple teams.
This gives us an infinite number of possibilities to write elegant, readable, and reusable code in the GraphQL world. This also adds a vast scope for organizational-level customizations and coding standards on the client side.
The next steps for the library would be to add support for GraphiQL, build a Babel plugin, a plugin for Apollo, add support for all types of AST node templates, modify all node types, and so on.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.