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.