Skip to content

Instantly share code, notes, and snippets.

@moroz
Last active January 20, 2021 02:49
Show Gist options
  • Save moroz/cb80b8d36fa8fdb4e5de323774b5e6a8 to your computer and use it in GitHub Desktop.
Save moroz/cb80b8d36fa8fdb4e5de323774b5e6a8 to your computer and use it in GitHub Desktop.
Transform errors in Absinthe graphql mutations
defmodule MyAppWeb.Api.Middleware.RestrictAccess do
@behaviour Absinthe.Middleware
@moduledoc """
A simple Absinthe middleware to ensure that GraphQL queries or mutations
are always run by authorized users.
"""
def init(default), do: default
def call(%{context: %{current_user: %MyApp.Users.User{}}} = res, _config) do
res
end
def call(res, _config) do
%{res | errors: ["You are not signed in."], state: :resolved}
end
end
defmodule MyAppWeb.Schema do
use Absinthe.Schema
alias MyAppWeb.Api.Middleware.RestrictAccess
# ...
def middleware(middleware, %{identifier: :current_user}, %{identifier: :query}) do
middleware
end
def middleware(middleware, %{identifier: :sign_in}, %{identifier: :mutation}) do
middleware
end
def middleware(middleware, _field, %Absinthe.Type.Object{identifier: :mutation}) do
[RestrictAccess | middleware] ++ [MyAppWeb.Api.Middleware.TransformErrors]
end
def middleware(middleware, _field, %Absinthe.Type.Object{identifier: :query}) do
[RestrictAccess | middleware]
end
def middleware(middleware, _, _) do
middleware
end
end
defmodule MyAppWeb.Api.Middleware.TransformErrors do
@behaviour Absinthe.Middleware
import Absinthe.Utils
alias MyAppWeb.ErrorHelpers
def call(res, _) do
with %{errors: [error]} <- res do
%{res | errors: [], value: handle_error(error)}
end
end
def handle_error(%Ecto.Changeset{} = changeset) do
%{success: false, errors: transform_errors(changeset)}
end
def handle_error(errors) when is_map(errors) do
%{success: false, errors: errors}
end
def handle_error(str) when is_binary(str) do
%{success: false, errors: %{"message" => str}}
end
defp transform_errors(changeset) do
changeset
|> remove_replace_content_changesets()
|> Ecto.Changeset.traverse_errors(&ErrorHelpers.translate_error/1)
|> normalize_map()
end
defp remove_replace_content_changesets(changeset) do
case Map.get(changeset, :changes) do
changes = %{content: content} when is_list(content) ->
new_content = for %{action: :insert} = slice <- content, do: slice
changes = %{changes | content: new_content}
%{changeset | changes: changes}
_ ->
changeset
end
end
defp format_nested_map(map) do
Enum.reduce(
map,
%{},
fn {key, [error]}, acc ->
case String.split(to_string(key), ".") do
[key] ->
Map.put(acc, key, error)
[top, nested] ->
Map.update(acc, top, %{nested => error}, &Map.put(&1, nested, error))
end
end
)
end
defp normalize_error(list) when is_list(list) do
case Enum.all?(list, &is_binary/1) do
true ->
Enum.join(list, " ")
false ->
case Enum.all?(list, &is_map/1) do
true ->
Enum.with_index(list)
|> Map.new(fn {map, index} -> {index, format_nested_map(map)} end)
_ ->
nil
end
end
end
defp normalize_error(map) when is_map(map) do
normalize_map(map)
end
defp normalize_map(map) do
Map.new(map, fn {key, value} -> {format_key(key), normalize_error(value)} end)
end
defp format_key(key) do
key
|> to_string
|> camelize(lower: true)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment