View Source Upgrading to v1.4

This information is extracted and expanded from the CHANGELOG.

This version included subscriptions, and also came packaged with a number of improvements that required breaking changes.

The breaking changes primarily affect middleware and plugin authors, but some changes (like null handling and changes to error messages) warrant review by all Absinthe users.

Middleware: Watch Out for Eager Default

Default middleware are now applied eagerly. Although a small change, this will affect anyone who is currently changing the default middleware.

Before v1.4

Before v1.4, the default middleware was applied "lazily". What this means is if you had a simple field like:

object :user do
  field :name, :string
end

then when it is passed to the middleware/3 callback on a schema, the middleware is an empty list:

def middleware(middleware, %{identifier: :name}, %{identifier: :user}) do
  middleware |> IO.inspect
  #=> []
end

The nice thing about this was that it made it easy to pattern match for the "no middleware supplied" case; you could just match against [].

The problem, however, is that if you wanted to add a simple tracing middleware that runs on every field for example, the "obvious" way to do it seemed like it could be this:

def middleware(middleware, _field, _object) do
  [YourTracer | middleware]
end

... and we just broke our field :name, :string field, and all others like it. No longer did it come back from def middleware as [], and thus our lazy default wasn't applied.

This has tripped up an unreasonable number of people, and it violated the common sense meaning of "default value." It also made it hard for users to determine what default middleware might run on a field. Checking the value passed to middleware/3 didn't help.

In v1.4

The default middleware is now eager:

def middleware(middleware, %{identifier: :name}, %{identifier: :user}) do
  middleware |> IO.inspect
  #=> [{Absinthe.Middleware.MapGet, :name}]
end

Every field has at least one middleware specified, and the middleware/3 callback has full access to it. Conceptually, it's a lot simpler than the previous approach, in that there isn't some hidden action after the fact that you can't see.

Changing the default is a bit more work now, because you have to explicitly match against the Absinthe default to put something else in its place. However it isn't very difficult (moreover a helper function could be added later to make it even easier).

Plugins: Change Arguments

Plugins now receive an %Absinthe.Blueprint.Execution{} struct instead of the bare accumulator. This makes it possible for plugins to set or operate on context values. Upgrade your plugins! Change this:

def before_resolution(acc) do
  acc = # doing stuff to the acc here
end
def after_resolution(acc) do
  acc = # doing stuff to the acc here
end
def pipeline(pipeline, acc) do
  case acc do
    # checking on the acc here
  end
end

to:

def before_resolution(%{acc: acc} = exec) do
  acc = # doing stuff to the acc here
  %{exec | acc: acc}
end
def after_resolution(%{acc: acc} = exec) do
  acc = # doing stuff to the acc here
  %{exec | acc: acc}
end
def pipeline(pipeline, exec) do
  case exec.acc do
    # checking on the acc here
  end
end

The reason for this is that you can also access the context within the exec value. When using something like Dataloader, it's important to have easy to the context

Calling All Resolvers: The Null Literal Has Arrived

Absinthe now supports GraphQL null literals.

null values, when provided as arguments, are passed on to Absinthe resolvers as nil (provided they don't run afoul of a non_null/1 argument constraint).

The concrete effect of this is that your resolvers may need to be updated to take into account that nil is a possible value; in the past arguments would never be passed as nil.

Let's take a look at this example schema snippet.

Before v1.4

field :avatar_url, :string do
  arg :size, :integer, default_value: 64
  # ... resolver here
end

Before v1.4, you could write a resolver that could always assume a :size would be provided:

resolve fn _, %{size: size}, _ ->
  {:ok, "http://example.com/avatars/test_#{size}x#{size}.png"}
end

If the user doesn't provide a value for :size, it would default to 64:

{
  avatarUrl(size: 32) # => http://example.com/avatars/test_32x32.png
  avatarUrl           # => http://example.com/avatars/test_64x64.png
  # This is invalid in v1.3, as there's no `null` literal:
  avatarUrl(size: null)
}

In v1.4

In v1.4, users can freely send null, and that value will override any default:

{
  # This didn't change:
  avatarUrl(size: 32)   # => http://example.com/avatars/test_32x32.png
  # Neither did this (default value applied as expected)
  avatarUrl             # => http://example.com/avatars/test_64x64.png
  # But `nil` gets interpolated here, as it overrode the default value of `64`:
  avatarUrl(size: null) # => http://example.com/avatars/test_x.png
}

You may want to change your resolvers to use the default value if nil is received:

resolve fn _, %{size: size}, _ ->
  px = size || 64
  {:ok, "http://example.com/avatars/test_#{px}x#{px}.png"}
end

Or handle it as an explicit match (especially if you want to assign a special semantic value to the now-available null value):

resolve fn
  _, %{size: nil}, _ ->
    {:ok, "http://example.com/avatars/test_default.png"}
  _, %{size: size}, _ ->
    {:ok, "http://example.com/avatars/test_#{size}x#{size}.png"}
end

Some other options to think about:

  • Shore up your schema by adding more uses of non_null/1 constraints where it makes sense.
  • Add piece of middleware to strip out nil argument values, universally or focused on a specific set of fields.

Expect Shorter Error Messages

Errors returned from resolvers no longer say "In field #{field_name}:". The inclusion of the path information obviates the need for this data, and it makes error messages a lot easier to deal with on the frontend.

If you expect certain error messages in your clients, you may need to update your code.