View Source Absinthe Query All

Absinthe Query All is a tool for comprehensively and automatically querying and testing your Absinthe GraphQL schema.

You can read the story of how the core functionality of this package was developed here.

Installation

If available in Hex, the package can be installed by adding absinthe_query_all to your list of dependencies in mix.exs:

def deps do
  [
    {:absinthe_query_all, "~> 0.5.0"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/absinthe_query_all.

How to use

This core AbsintheQueryAll module provides two utility functions.

the get_all_operations function

This function returns a list of every operation of the specified type (i.e. query or mutation) in your absinthe graphql schema.

AbsintheQueryAll.get_all_operations(SomeAbsintheSchema, :query)
[
  :someQuery, :someOtherQuery, :someThirdQuery
]

the get_comprehensive_response function

The get_comprehensive_response function runs the graphql operation specified, requesting a response that contains every possible field and subfield.

AbsintheQueryAll.get_comprehensive_response(%{
  schema: SomeAbsintheSchema,
  operation_type: :query,
  operation_name: :someOperation,
})
{:ok, %{data: %{"someField" => 1, "someOtherField" => 2, "someThirdField" => 3}}}

Automatic comprehensiveness checking of your absinthe resolve callbacks

Why these two functions? So that you can set them up like this:

describe "SomeAbsintheSchema" do
  # First we get every single query available in the schema
  for operation <- AbsintheQueryAll.get_all_operations(SomeAbsintheSchema, :query) do
    # Then we create a test case
    test "query `#{operation}` responds successfully" do
      # Inside the test case, we run the query
      {status, response} = AbsintheQueryAll.get_comprehensive_response(%{
        schema: TestAbsintheSchema,
        operation_type: :query,
        operation_name: unquote(Macro.escape(operation)),
      })

      # And then we check to see that it returned successfully
      assert status == :ok
      assert Map.has_key?(response, :data)
      refute Map.has_key?(response, :errors)
    end
  end

  # Then we do the same thing with our mutations
  for operation <- AbsintheQueryAll.get_all_operations(SomeAbsintheSchema, :mutation) do
    test "mutation `#{operation}` responds successfully" do
      {status, response} = AbsintheQueryAll.get_comprehensive_response(%{
        schema: TestAbsintheSchema,
        operation_type: :mutation,
        operation_name: unquote(Macro.escape(operation)),
      })

      assert status == :ok
      assert Map.has_key?(response, :data)
      refute Map.has_key?(response, :errors)
    end
  end
end

With that setup, your app will automatically generate a test for every single graphql operation and make sure that a response is being generated for every single field in that operation. Just a nice, simple extra layer protection against a specific type of bug.

Optional arguments to AbsintheQueryAll.get_comprehensive_response and Query.comprehensive

Generating Argumentss

Your schema likely has operations that require arguments. In order to generate arguments to pass to those operations, the get_comprehensive_response function has an optional argument, argument_generator, that you can pass an argument generating function to. The argument generating function should take two arguments: the first is the operation_name (e.g. :someQuery) and the second is the name of the argument you're generating (as an atom; e.g. :userId).

For example, you might call get_comprehensive_response like this:

AbsintheQueryAll.get_comprehensive_response(%{
  schema: SomeAbsintheSchema,
  operation_type: :query,
  operation_name: :testQuery,
  argument_generator: fn operation_name, argument_name ->
    case {operation_name, argument_name} do
      {:someQuery, :theNameOfSomeStringArgumentsToTheQuery} ->
        # if this argument requires a string, here's where you choose what string it gets
        "some string"
      {:someQuery, :theNameOfSomeIntegerArgumentsToTheQuery} ->
        7

    end 
})

Passing in Context

The optional context argument on get_comprehensive_response is just for passing along the context map that Absinthe.run takes. You might use this for something like passing in information on the currently logged in user if that has an effect on what your graphql endpoint will return for some operations.

AbsintheQueryAll.get_comprehensive_response(%{
  schema: SomeAbsintheSchema,
  operation_type: :query,
  operation_name: :testQuery,
  context: %{current_user: user}
})

Restricting fields in the response

If you would like a response that does not contain every possible field, you can pass the permitted_fields option to get_comprehensive_response. This option should be a map. It is a map instead of a list so that it can facilitate nesting of the permitted fields specification.

For example, the following code

AbsintheQueryAll.get_comprehensive_response(%{
  schema: SomeAbsintheSchema,
  operation_type: :query,
  operation_name: :testQuery,
  permitted_fields: %{someField: nil, someOtherField: %{someNestedField: nil}}
})
"query testQuery {\n  testQuery { someField someOtherField { someNestedField } }\n}\n"

presumes that someOtherField is an object with fields of its own, and will generate a query that only requests the someField and someOtherField fields at the top level, and only the someNestedField field inside the someOtherField object.

Aliasing Options

The option to pass parent_alias_generator and leaf_alias_generator functions will most likely not be useful to most people, unless you, (A) specifically need to test something involving aliases, or (B) are using the utility functions available in this package to interface with tools like elm-graphql that automatically alias fields. But they are there if you need them.

iex> Query.comprehensive(%{
...>   schema: SomeAbsintheSchema,
...>   operation_type: :query,
...>   operation_name: :testQuery,
...>   parent_alias_generator: fn _field_name, _field_args -> "someAlias" end
...> })
"query testQuery {\n  someAlias: testQuery { someField }\n}\n"
iex> Query.comprehensive(%{
...>   schema: SomeAbsintheSchema,
...>   operation_type: :query,
...>   operation_name: :testQuery,
...>   leaf_alias_generator: fn field_name, _field_details -> "#{field_name}Alias" end
...> })
"query testQuery {\n  testQuery { someFieldAlias: someField }\n}\n"

Other possibilities

This library intentionally exposes a lot of the underlying modules/functions that make the two main functions work, in case anyone wants to use them to build other tools. I use them for a variety of nice graphql testing functions that I may take a stab at generalizing and open-sourcing in the future if I find the time.