Absurdist Arcana Ben Weintraub's blog

gquil: a CLI tool for exploring GraphQL schemas

The 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:

  1. 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.
  2. 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.
  3. It’s quite verbose compared to the SDL format.
  4. 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 of gquil 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 an underlyingTypeName 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 an underlyingTypeName of String. There’s also a typeName 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:

A portion of the GraphViz visualization of the full GitHub GraphQL API

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:

A GraphViz visualization of the RepositoryRule.parameters field, and all types that it references

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

A GraphViz rendering of the built-in GraphQL introspection schema

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

A GraphViz rendering showing the schema elements reachable within 1 hop from Mutation.addPullRequestReview or Mutation.submitPullRequestReview

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.