GraphQL Pro 1.3.0 adds support for server-defined queries via
GraphQL::Pro::Repository. In this approach, GraphQL operations are stored on the server and clients invoke them by name.
This provides several benefits:
- You can completely close the door to client-provided query strings. This removes an attack vector for a malicious client who might try to swamp your system with expensive queries.
- Static queries (in
.graphqlfiles) are easier to review and more available tooling (eg, code generation or analysis).
- Operation names improve GraphQL server monitoring by serving as the primary unit of analysis.
What’s a “repository?”
GraphQL::Pro::Repository works like a single, large GraphQL document with many different operations (ie, queries, mutations, or subscriptions) and fragments inside it. These operations are validated and analyzed as a single unit, as if they came in a single query string.
From a client’s perspective, the server has a fixed set of operations it can perform. Each one can be executed by sending its operation name.
The repository approach allows us to use pre-existing GraphQL concepts:
- Document: A GraphQL document is a set of operations and fragments. The semantics of a valid document are well-specified and broadly implemented. A repository is an extension of this concept.
- Operation name: GraphQL includes a way to specify which operation to run in a document. Repositories build on this by separating the set of operations (which lives on the server) from the identifier (which comes from the client).
By employing these concepts, we make full use of the battle-tested graphql-ruby runtime without deviating from the spec.
A Quick Example
First, add a
.graphql file with a named operation:
1 2 3 4 5 6 7
Then, define a repository with that path:
1 2 3 4
Next, update your controller to execute queries with the repository instead of the schema:
1 2 3 4 5 6 7 8
Finally, execute the operation by sending a request with the
1 2 3 4 5 6 7 8 9 10
🎉 We served a GraphQL response by name!
A straightforward approach is to name
.graphql files after the operations they contain, so this operation:
1 2 3 4 5
would go in:
This way, a reader can skim the
app/graphql/documents directory to take a quick inventory of operations. Also, this one-to-one mapping mimics the Ruby convention of putting constants in identically-named files.
In the end,
GraphQL::Pro::Repository will accept files with any name, as long as they match
Since a repository functions as one big GraphQL document, fragments are shared by default.
You can put fragments in their own files, then reference them from each operation that needs them. This way, operations with common data responsibilities can share code, ensuring that they stay in sync.
For example, consider a list of comments with a box to create a new comment. We’d make three
1 2 3 4
First, specify the operation to load the list of comments:
1 2 3 4 5 6 7 8 9 10 11 12 13
Then, specify the operation to create a new comment:
1 2 3 4 5 6
After creating a comment, you want to update the list of comments to include the new member. To express this shared need for data, create a fragment with the required fields:
1 2 3 4 5 6 7 8 9
Then, apply the fragment to
1 2 3 4 5 6 7 8 9 10 11 12 13 14
1 2 3 4 5 6 7
- A reader can see that these operations are linked
- If the list view ever requires more data, the create operation will load that data, too
- Repositories can also accept dynamic inputs. This allows you to use GraphiQL during development or continue serving old clients while you transition to server-defined queries.
- On Rails, repositories watch their files and reload as needed. If you’re using another framework, you can reload repositories as needed.
- You can use a repository to find unused fields in your schema.
For me, I’m hoping to improve client support (eg, Apollo Client) and server tooling (eg, query diffing) to make repositories even more useful!