Editor's note: This is a cross-post written by Senior Software Developer, Manuel Spigolon. Manuel has his own blog at backend.cafe where you can subscribe for updates and find more great posts. Some of the links in the article point to Manuel’s personal GitHub account.
A real-world example of advanced usages
If you're using Fastify with Mercurius as your GraphQL adapter, you may be looking for some advanced usages. In this article, we'll explore a real-world example with Dynamic GQL queries with Mercurius.
“The example we will discuss is only one scenario that you may encounter as Software Engineer at NearForm! If you want to solve these kind of problems,we are hiring!”
What are Dynamic GraphQL queries?
A dynamic GraphQL query is a query that is constructed at runtime, based on the needs of the client. This is useful when the client is uncertain of the structure of the data and needs to retrieve specific data based on conditions, or when the client needs to retrieve a subset of data depending on the user's role or permissions.
In our use case, we want to invert the control of query dynamicity from the client to the server. This means that the client will send a standard GQL query and the server will return the data based on:
The client's query
The user's role
It's important to note that we are not talking about returning a subset of a generic GraphQL type, but a completely different GraphQL type.
Defining the schema
When creating a GraphQL server, the first step is to define the schema. The GraphQL specification provides clear guidance on how to accomplish our target by utilizing GraphQL Unions .
Unions in GraphQL allow for multiple types to be returned from a single field, making it a powerful tool for querying related data. This can be especially useful when retrieving data from multiple types in a single query.
To begin, let's define our schema using GraphQL Unions.
As you can see, our schema includes a Query type with a searchData field that returns a Grid union type. This Grid union type can represent one of three possible types: AdminGrid, ModeratorGrid, or UserGrid.
While this is a valid query, it is not the desired outcome . We aim to return a different type based on the user's role, so that the client can send a query like this:
It's important to note that the above query is not valid against the schema defined above, but with some additional implementation, we can make it work!
Implementing the business logic
Let's start building the basic implementation of our server. You can skip this section if you are already familiar with Fastify and Mercurius.
The first step is to install the necessary dependencies.
Now, copy the schema we defined above in a gql-schema.js file, then you need to create an app.js file where we will write our server:
To verify that everything is working, you can start the server running node app.js run and you should see the following output: Server listening at http://127.0.0.1:8080 Great! Now we must list all the use cases we want to implement by adding some test cases.
Testing our server
We want to test our server with the following use cases:
A user with the admin role should be able to retrieve the totalRevenue field without inline fragments
A user with the moderator role should be able to retrieve the banHammer field without inline fragments
A user with the user role should be able to retrieve the basicColumn field without inline fragments
A user without the admin role should not be able to retrieve the totalRevenue field
A user without the moderator role should not be able to retrieve the banHammer field
A user without the user role should not be able to retrieve the basicColumn field
A user without any role should not be able to retrieve any field
We must install the required dependencies:
npm install tap@15 -D We can write these tests in a test.js file.
For the sake of simplicity, we will not list all the tests here, but you can find the complete source code in the GitHub repository .
Running the tests with node test.js will fail because we have not implemented the business logic yet. So, let's start writing the code!
Implementing the server-side Dynamic Queries
To implement the business logic, there are these main steps:
Retrieve the user's role from the request headers
Manage the GraphQL query to return the correct type based on the user's role
Let's solve the first point.
How to retrieve the user's role
We can implement the user role retrieval by installing the mercurius-auth plugin.
npm i mercurius-auth@3 Then, we can register the plugin in our app.js file. To understand what the plugin does, you can read its documentation.
In the following example, we will compare the x-user-type HTTP header with the @auth directive we are going to define in the schema. If they match, the user will be authorized to access the field and run the query.
Let's start by defining the @auth directive in the schema:
Then, we can register the plugin in our app.js file and implement a simple searchData resolver:
Now, the user should be able to retrieve the totalRevenue field only if the x-user-type header is set to admin .
Nevertheless, we can't run the tests yet because we have not implemented the second point.
Implementing the Dynamic Queries
The last step is to implement the dynamic queries.
Right now, our tests are failing with the following error:
“"Cannot query field 'totalRevenue' on type 'Grid'. Did you mean to use an inline fragment on 'AdminGrid'?”
The error is correct because we are not using inline fragments to query a Union type.
To overcome this issue, we can use the mercurius preValidation hook. Let's try to see how it works:
When a client sends a GraphQL query, the server will process the GQL Document in the preValidation hook, before validating the GQL against the GQL Schema.
In this hook, we can modify the GQL Document sent by the user to add the inline fragments. So, after the mercurius-auth plugin registration, we can add the following code:
In the previous code, we inspect the GraphQL Document Abstract Syntax Tree (AST) to determine if we need to add the inline fragment programmatically. If we recognize the query as a masked query, we add the inline fragment to the AST and return the modified AST to the GraphQL Document.
By doing this, Mercurius will process the modified GraphQL Document as if the user had sent it with the inline fragment. This means that it will:
Validate the GraphQL Document against the GraphQL Schema
Apply the authentication policy
Resolve the query
With this implementation, when running tests, everything should pass successfully.
We have seen how versatile Mercurius is and how simple it is to implement features like dynamic queries in a complex scenario. While the goal may seem challenging, this server-side implementation provides several benefits such as:
Avoiding breaking changes on the client side to support new features
Hiding GraphQL Types from the client through disabling schema introspection
Applying query optimizations for the client
Providing a specialized response based on the user type instead of a generic one.
This is just a small example of the possibilities when using Mercurius and manipulating the GraphQL Document.