gquil: a CLI tool for exploring GraphQL schemas
13 Jun 2024The GraphQL ecosystem has mature tools for browsing GraphQL schemas in a web browser (e.g. GraphiQL) or GUI app (e.g. altair). These tools are great, but in terms of flexibility and composability, nothing beats the command line for me. While working on the server-side of a complex GraphQL service, I repeatedly found myself wanting a CLI to help make sense of it. Something that I could run without leaving the terminal environment, and pipe to other commands I use frequently (grep
, awk
, sort
, jq
, etc.).
There are tools like graphql-cli
out there, but I found the initial setup process for graphql-cli to be somewhat overwrought. This isn’t to say that graphql-cli is bad - it looks like a well-thought-out and higly extensible tool, but I think my philosophy on tool design is different from that of the authors: when the entrypoint to a CLI tool is a configuration wizard, I know that it’s not for me.
So, I wrote my own CLI tool for working with GraphQL schemas: gquil.
The rest of this post walks through a few examples of how to use gquil
, using the GitHub GraphQL API to demonstrate.
Getting the schema
GitHub has some nice documentation on how to obtain the full GraphQL schema for their API. They offer several options for doing so, but if you want to obtain the schema in GraphQL SDL form, you can do something like this:
❯ curl -H "Authorization: bearer TOKEN" \
-H "Accept: application/vnd.github.v4.idl" \
https://api.github.com/graphql | jq -r .data
This will yield a ~60k-line document in the GraphQL schema language (aka ‘GraphQL SDL’), describing all of the types, fields, interfaces, enums, etc. that comprise GitHub’s GraphQL API.
GraphQL SDL vs. introspection JSON
GraphQL schemas can also be queried using GraphQL itself, via something called the introspection schema. Introspection query responses can be serialized to JSON, leading to an alternative mechanism for representing the schema.
However, there are a few drawbacks to this ‘introspection JSON’ format:
- Since GraphQL uses client-specified response shapes, there isn’t really a single JSON schema that describes the exact shape of ‘introspection JSON’ in all cases. What you get back depends on the introspection query you write! The reference GraphQL JS implementation does export an
introspectionQuery
variable, but it’s not part of the official GraphQL spec, which describes the introspection schema, but leaves queries against it up to you. - The introspection schema omits information about the application sites of custom directives within a schema (Apollo calls directives applied to schemas ‘schema directives’ to differentiate them from schemas applied to operations). This directive information is sometimes of great interest to schema maintainers.
- It’s quite verbose compared to the SDL format.
- The JSON representation of the introspection schema uses variable levels of nesting to represent things like wrapping types (lists and non-nullable types). This aligns with GraphQL’s hierarchical nature, but makes processing of it with tools like
jq
harder than it should be (more on this later).
GraphQL servers almost universally support the introspection schema (though public access to it may be disabled for security reasons), but not every GraphQL service necessarily has a GraphQL SDL representation of its schema. This is especially true of ‘code first’ GraphQL servers, where the schema is defined programatically in the native programming language of the server, rather than being specified in GraphQL SDL to begin with.
Generating SDL from an introspection endpoint
For these reasons, gquil
makes it easy to generate a GraphQL SDL representation of a schema from an introspection endpoint. Here’s an example of how to do this with the GitHub API:
❯ gquil introspection generate-sdl \
-H "Authorization: bearer TOKEN" \
https://api.github.com/graphql
… where TOKEN
is a GitHub auth token. The -H
/ --header
flag works similarly curl
’s, allowing you to pass in any headers that might be needed to authenticate against the server.
You can also see the exact query being executed against the server like this:
❯ gquil introspection query
query IntrospectionQuery {
__schema {
queryType {
name
}
... snip ...
… or by adding the --trace
flag to an invocation of introspection generate-sdl
, which will emit details of the outbound request and inbound response to stderr.
Listing schema elements
Sometimes, you just want to see what’s in a GraphQL schema. For this, gquil ls
is your friend.
Listing types
You can list types (scalars, objects, interfaces, unions, enums, and input objects):
❯ gquil ls types github.graphql
INPUT_OBJECT AbortQueuedMigrationsInput
OBJECT AbortQueuedMigrationsPayload
INPUT_OBJECT AbortRepositoryMigrationInput
... snip ...
You can optionally filter the list by kind, like this:
❯ gquil ls types --kind interface github.graphql
Actor
AnnouncementBanner
Assignable
... snip ...
You can also do things like listing all types which implement a given interface:
❯ gquil ls types --implements Actor github.graphql
OBJECT Bot
OBJECT EnterpriseUserAccount
OBJECT Mannequin
OBJECT Organization
OBJECT User
… or which belong to a given union:
❯ gquil ls types --member-of PinnableItem github.graphql
OBJECT Gist
OBJECT Repository
Listing fields
You can also list fields (on objects, input objects, or interfaces), like this:
❯ gquil ls fields github.graphql
AbortQueuedMigrationsInput.clientMutationId: String
AbortQueuedMigrationsInput.ownerId: ID!
AbortQueuedMigrationsPayload.clientMutationId: String
... snip ...
You can limit the set of returned fields to only those defined on a specific type:
❯ gquil ls fields --on-type Actor github.graphql
Actor.avatarUrl: URI!
Actor.login: String!
Actor.resourcePath: URI!
Actor.url: URI!
Sometimes you want to see all of the ways in which a particular type is used within a schema. For that, you can use --of-type
to only return fields which are of a specified type (or a wrapped variant of that type):
❯ gquil ls fields --of-type User github.graphql
AddEnterpriseOrganizationMemberPayload.users: [User!]
AddedToMergeQueueEvent.enqueuer: User
AssignedEvent.user: User
... snip ...
Note how AddEnterpriseOrganizationMemberPayload.users
is returned, even though its type is [User!]
rather than User
.
Sometimes, you want to see all fields which might possibly return a given type. For example, in the GitHub API, there’s an interface called Actor
, which is implemented by multiple types, including User
. If we ask for all fields of type User
(or a wrapped type thereof), we see that there are 148 of them:
❯ gquil ls fields --of-type User github.graphql | wc -l
148
… but what about fields which are typed as Actor
instead? These fields might possibly return an instance of User
, but are excluded from the above listing. To include them, we can use --returning-type
instead, which accounts for interface and union types:
❯ gquil ls fields --returning-type User github.graphql | wc -l
369
We can also filter fields by name using the --named
flag. Sometimes, fields are named the same, even though they’re of different types. For example, in the GitHub API, we can see that fields named user
are usually typed as User
(or User!
), but there is one case where a field of this name is typed as Actor
instead!
❯ gquil ls fields --named user github.graphql \
| awk '{ print $2 }' \
| sort \
| uniq -c
1 Actor
74 User
12 User!
Let’s find out which one that is:
❯ gquil ls fields --named user --of-type Actor github.graphql
SavedReply.user: Actor
Listing directives
GraphQL also supports custom directives that can be attached to various parts of a schema. Vendors like Apollo use these directives heavily for supporting federation. You can list all custom directives within a schema too, like this:
❯ gquil ls directives github.graphql
@possibleTypes(abstractType: String, concreteTypes: [String!]!) on INPUT_FIELD_DEFINITION
@preview(toggledBy: String!) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
@requiredCapabilities(requiredCapabilities: [String!]) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
JSON output
All of the ls
subcommands described above default to a compact, line-based output format suitable for use with grep
, awk
, sort
, etc. Sometimes, you want more information about each listed item, and more flexibility in terms of how to process the list. For those cases, all ls
subcommands also support a JSON output format via the --json
flag.
Returning to an example from above, let’s say we wanted to find the types of all fields which might possibly return a value of type User
. Previously, we had this command to find all fields which might return a User
:
❯ gquil ls fields --returning-type User github.graphql
Adding --json
to that incantation, we get output like this:
❯ gquil ls fields --returning-type User --json github.graphql
[
{
"description": "The subject",
"name": "AddCommentPayload.subject",
"type": {
"kind": "INTERFACE",
"name": "Node"
},
"typeName": "Node",
"underlyingTypeName": "Node"
},
{
"description": "The users who were added to the organization.",
"name": "AddEnterpriseOrganizationMemberPayload.users",
"type": {
"kind": "LIST",
"ofType": {
"kind": "NON_NULL",
"ofType": {
"kind": "OBJECT",
"name": "User"
}
}
},
"typeName": "[User!]",
"underlyingTypeName": "User"
},
... snip ...
We can use this output in combination with a generic JSON processing tool like jq
to answer our question as follows:
❯ gquil ls fields --returning-type User --json github.graphql \
| jq '[.[] | .underlyingTypeName] | unique'
[
"Actor",
"Assignee",
"AuditEntryActor",
"BranchActorAllowanceActor",
"Claimable",
... snip ...
Details of the JSON output format
The JSON output format for gquil
is inspired by GraphQL’s introspection schema, but differs in a few important ways:
- When you ask for the type of a field in the introspection schema, you get back a full
__Type
object, allowing you to recursively traverse the type graph. This makese sense if you’re writing GraphQL queries against a schema, but is difficult to shoehorn into a model where you’re emitting JSON in a standardized shape (where do you stop the recursion?). For this reason, references to other named types in the JSON output format ofgquil
are just strings which name the referred-to type. - The JSON output format for
gquil
includes information about directives that have been applied at various places within the schema (this information is mostly omitted from the introspection schema, with a few special-case exceptions for built-in directives like@deprecated
). Sometimes, information about the set of applied directives is useful for filtering and processing schema elements (e.g. when you want to find all fields that have a specific directive applied to them). - As a convenience, the
gquil
JSON output format adds anunderlyingTypeName
field to its representation of GraphQL fields, which contains the ‘unwrapped’ type name of the field. For example, a field of type[String!]
would have anunderlyingTypeName
ofString
. There’s also atypeName
field, which contains the string representation of the field’s type, with any wrapping decorations (e.g.[String!]
for the previous example).
Graph reachability filtering
Sometimes it’s interesting to be able to answer questions like “what is the full set of types needed to represent anything that might possibly be returned by this field?”. This might come up, for example, when you’re considering extracting parts of your GraphQL schema into a subgraph.
For example, here’s how you could list all fields which are reachable if you start from the Query.license
entrypoint within the GitHub schema:
❯ gquil ls fields --from Query.license github.graphql
License.body: String!
License.conditions: [LicenseRule]!
License.description: String
License.featured: Boolean!
License.hidden: Boolean!
License.id: ID!
License.implementation: String
License.key: String!
License.limitations: [LicenseRule]!
License.name: String!
License.nickname: String
License.permissions: [LicenseRule]!
License.pseudoLicense: Boolean!
License.spdxId: String
License.url: URI
LicenseRule.description: String!
LicenseRule.key: String!
LicenseRule.label: String!
Query.license: License
You can also start from a type instead of a field, like this:
❯ gquil ls fields --from License github.graphql
License.body: String!
License.conditions: [LicenseRule]!
License.description: String
... snip ...
LicenseRule.label: String!
This style of filtering can be applied to both ls fields
and ls types
:
❯ gquil ls types --from Query.license github.graphql
OBJECT License
OBJECT LicenseRule
OBJECT Query
SCALAR URI
You can also specify the --from
flag multiple times in order to use multiple starting points. For example, to show all types reachable from Query.license
or Query.codeOfConduct
:
❯ gquil ls types --from Query.license --from Query.codeOfConduct github.graphql
OBJECT CodeOfConduct
OBJECT License
OBJECT LicenseRule
OBJECT Query
SCALAR URI
Finally, you can use the --depth
flag with --from
in order to limit the depth of the traversal from the root. A depth of 1 means only the referred to object itself:
❯ gquil ls types --from VerifiableDomainOwner --depth 1 github.graphql
UNION VerifiableDomainOwner
Increasing the value of the --depth
parameter includes more distantly-related types & fields:
❯ gquil ls types --from VerifiableDomainOwner --depth 2 github.graphql
OBJECT Enterprise
OBJECT Organization
UNION VerifiableDomainOwner
❯ gquil ls types --from VerifiableDomainOwner --depth 3 github.graphql | wc -l
82
Visualizing schemas with GraphViz
Finally, the good stuff. Producing lists of schema elements in line-delimited or JSON format is nice, but sometimes, you need a picture. For that, there’s gquil viz
, which can emit visualizations of your GraphQL schema in GraphViz DOT format.
First, if you’ve never used GraphViz, run, don’t walk to your local package manager:
❯ brew install graphviz
This will give you (among other things) a tool called dot
, which can turn simple textual representations of graphs into pretty pictures. You can now use this tool in combination with gquil
plus your GraphQL schema.
A note of caution: big schemas = big visualizations
If you have a non-trivial GraphQL schema, and just try to run it through gquil viz
and then render the resulting graph, it is very likely to yield an unintelligible tangle that takes forever to render.
For example, here’s how the GitHub schema looks when rendered by gquil
with GraphViz:
❯ time gquil viz github.graphql | dot -Tpng >out.png
dot: graph is too large for cairo-renderer bitmaps. Scaling by 0.361918 to fit
gquil viz github.graphql 0.13s user 0.03s system 118% cpu 0.130 total
dot -Tpng > out.png 305.79s user 3.88s system 98% cpu 5:13.85 total
After about 5 minutes, this command produces an 88 MB(!) PNG file, which you can get the flavor of from the cropped section below:
Trimming the visualization
To get a useful visualizaiton, you need to get more specific about what you want to see. To support this, the viz
subcommand supports the same --from
and --depth
flags described above in the section on listing schema elements.
For example, let’s say I wanted to visualize the possible values for the RepositoryRule.parameters
field:
❯ gquil viz --from RepositoryRule.parameters github.graphql \
| dot -Tpng >out.png
… produces a visualization like this:
For a fun trick, here’s a visualization of the built-in introspection schema defined in the GraphQL spec (in this example, we’re feeding an empty schema into gquil viz
on stdin, and using the --include-builtins
flag to force it to render built-in types):
❯ echo '' | gquil viz --include-builtins - | dot -Tpng >out.png
You can of course use the --depth
flag to limit the depth of traversal here too, and specify --from
multiple times to pull in multiple root fields or types:
❯ gquil viz --from Mutation.submitPullRequestReview \
--from Mutation.addPullRequestReview \
--depth 2 \
github.graphql \
| dot -Tpng >out.png
Feedback wanted!
I wrote gquil
originally for myself, but I’m curious to hear how it has worked (or not worked!) for you! If you happen to give it a try, I’d love to hear any feedback you have about the tool and how it could be better or more useful. See the contributing guide for details.