Skip to content
18 changes: 16 additions & 2 deletions apps/blunt_absinthe/lib/blunt/absinthe/absinthe_errors.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,25 @@ defmodule Blunt.Absinthe.AbsintheErrors do
Enum.map(messages, fn message -> [message: message] end) ++ acc

{key, messages}, acc when is_list(messages) or is_map(messages) ->
Enum.map(messages, fn message -> [message: "#{key} #{message}"] end) ++ acc
Enum.flat_map(leaves_with_path(messages, [key]), fn
{path, messages} ->
path = Enum.map(path, &to_string/1)
label = Enum.join(path, ".")
message = messages |> List.wrap() |> Enum.join(", ")

[[message: "#{label} #{message}", path: path]]
end) ++ acc

{key, message}, acc when is_binary(message) ->
[[message: "#{key} #{message}"] | acc]
[[message: "#{key} #{message}", path: [key]] | acc]
end)
|> Enum.map(&Keyword.merge(&1, extra_properties))
end

defp leaves_with_path(input, path \\ []) do
Enum.flat_map(input, fn {key, value} ->
full = [key | path]
if is_map(value), do: leaves_with_path(value, full), else: [{Enum.reverse(full), value}]
end)
end
end
4 changes: 4 additions & 0 deletions apps/blunt_absinthe/lib/blunt/absinthe/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ defmodule Blunt.Absinthe.Field do
def dispatch_and_resolve(operation, message_module, query_opts, parent, args, resolution) do
context_configuration = DispatchContextConfiguration.configure(message_module, resolution)

input_object = Keyword.get(query_opts, :input_object, false) || Keyword.get(query_opts, :input_object?, false)

args = Map.get(args, :input, args)

opts =
Expand All @@ -112,6 +114,8 @@ defmodule Blunt.Absinthe.Field do
return_value

{:error, errors} when is_map(errors) ->
errors = if input_object, do: %{input: errors}, else: errors

{:error, AbsintheErrors.format(errors)}

{:ok, %Context{} = context} ->
Expand Down
119 changes: 115 additions & 4 deletions apps/blunt_absinthe/test/blunt/absinthe/mutation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,28 @@ defmodule Blunt.Absinthe.MutationTest do
setup do
%{
query: """
mutation create($name: String!, $gender: Gender!){
createPerson(name: $name, gender: $gender){
mutation create($name: String!, $gender: Gender!, $address: AddressInput){
createPerson(name: $name, gender: $gender, address: $address){
id
name
gender
address {
line1
line2
}
}
}
""",
update_query: """
mutation update($input: UpdatePersonInput){
updatePerson(input: $input){
id
name
gender
address {
line1
line2
}
}
}
"""
Expand All @@ -34,12 +51,48 @@ defmodule Blunt.Absinthe.MutationTest do

test "can create a person", %{query: query} do
assert {:ok, %{data: %{"createPerson" => person}}} =
Absinthe.run(query, Schema, variables: %{"name" => "chris", "gender" => "MALE"})
Absinthe.run(query, Schema,
variables: %{"name" => "chris", "gender" => "MALE", "address" => %{"line1" => "42 Infinity Ave"}}
)

assert %{"id" => id, "name" => "chris", "gender" => "MALE"} = person
assert %{"id" => id, "name" => "chris", "gender" => "MALE", "address" => %{"line1" => "42 Infinity Ave"}} = person
assert {:ok, _} = UUID.info(id)
end

test "returns errors", %{query: query} do
assert {:ok, %{errors: [%{message: message}]}} =
Absinthe.run(query, Schema, variables: %{"name" => "chris", "gender" => ""})

assert message =~ "gender"
end

test "returns errors for absinthe type mismatch", %{query: query} do
assert {:ok, %{errors: [%{message: message}]}} =
Absinthe.run(query, Schema,
variables: %{
"name" => "chris",
"gender" => "MALE",
"address" => %{}
}
)

assert message =~ ~r/address.*\n.*line1.*found null/
end

test "returns errors from nested changeset validations", %{query: query} do
assert {:ok, %{errors: [%{message: message, path: path}]}} =
Absinthe.run(query, Schema,
variables: %{
"name" => "chris",
"gender" => "MALE",
"address" => %{"line1" => "10"}
}
)

assert message =~ "address.line1 should be at least 3 character(s)"
assert path == ~w(createPerson address line1)
end

test "user is put in the context from absinthe resolution context", %{query: query} do
context = %{user: %{name: "chris"}, reply_to: self()}

Expand All @@ -60,6 +113,9 @@ defmodule Blunt.Absinthe.MutationTest do
},
gender: %{
type: :gender
},
address: %{
type: :address_input
}
} = fields
end
Expand All @@ -68,4 +124,59 @@ defmodule Blunt.Absinthe.MutationTest do
assert %Object{fields: fields} = Absinthe.Schema.lookup_type(Schema, :dog)
assert %{name: %{type: :string}} = fields
end

test "returns errors from deeper nested changeset validations", %{update_query: update_query} do
assert {:ok, %{errors: [%{message: message, path: path}]}} =
Absinthe.run(update_query, Schema,
variables: %{
"input" => %{
"id" => UUID.uuid4(),
"name" => "chris",
"gender" => "MALE",
"address" => %{"line1" => "--"}
}
}
)

assert message =~ "address.line1 should start with a number, should be at least 3 character(s)"
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert message =~ "address.line1 should start with a number, should be at least 3 character(s)"
assert message =~ "input.address.line1 should start with a number, should be at least 3 character(s)"

Should the message include a fully qualified key path including the top level input object?
Or should the message only mention the last field (line1) and the path key include a full path?

Blunt.Absinthe.Field.dispatch_and_resolve maybe unwraps :input from args and AbsintheErrors doesn't have that knowledge if it should prefix the error key path with :input.

Just to compare, the error from Absinthe for a nil value resulting in a type mismatch is this:

Argument "input" has invalid value $input.
In field "address": Expected type "AddressInput", found {line1: null}.
In field "line1": Expected type "String!", found null.

assert path == ~w(updatePerson input address line1)
end

test "paths" do
errors = %{
a: %{
b: %{
c: %{
d: "broken"
},
cc: "fixed",
ccc: "unknown"
},
tree: ["trunk", "branches"]
}
}

{:ok, context} = DispatchContext.new(%Blunt.Absinthe.Test.CreatePerson{}, [])
context = DispatchContext.put_error(context, errors)
%{id: dispatch_id} = context

assert [
[message: "a.b.c.d broken", path: ~w(a b c d), dispatch_id: ^dispatch_id],
[message: "a.b.cc fixed", path: ~w(a b cc), dispatch_id: ^dispatch_id],
[message: "a.b.ccc unknown", path: ~w(a b ccc), dispatch_id: ^dispatch_id],
[message: "a.tree trunk, branches", path: ~w(a tree), dispatch_id: ^dispatch_id]
] = Blunt.Absinthe.AbsintheErrors.from_dispatch_context(context)
end

test "format errors with key in message" do
errors = %{input: %{person: %{address: %{stuff: %{thing: "everything is b0rked"}}}}}

assert [
[
message: "input.person.address.stuff.thing everything is b0rked",
path: ~w( input person address stuff thing ),
dispatch_id: "23424234"
]
] = Blunt.Absinthe.AbsintheErrors.format(errors, dispatch_id: "23424234")
end
end
14 changes: 14 additions & 0 deletions apps/blunt_absinthe/test/support/address.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule Blunt.Absinthe.Test.Address do
use Blunt.ValueObject
import Ecto.Changeset

field :line1, :string, required: true
field :line2, :string

@impl true
def handle_validate(changeset, _opts) do
changeset
|> validate_length(:line1, min: 3)
|> validate_format(:line1, ~r/(\d)+.*/, message: "should start with a number")
end
end
1 change: 1 addition & 0 deletions apps/blunt_absinthe/test/support/create_person.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Blunt.Absinthe.Test.CreatePerson do

field :name, :string
field :gender, :enum, values: Person.genders(), default: :not_sure
field :address, Blunt.Absinthe.Test.Address, required: false

internal_field :id, :binary_id, desc: "Id is set internally. Setting it will have no effect"

Expand Down
4 changes: 4 additions & 0 deletions apps/blunt_absinthe/test/support/create_person_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ defmodule Blunt.Absinthe.Test.CreatePersonHandler do
def handle_dispatch(command, _context) do
command
|> Map.from_struct()
|> Map.update!(:address, &to_map/1)
|> Person.changeset()
|> Repo.insert()
end

defp to_map(nil), do: nil
defp to_map(value), do: Map.from_struct(value)
end
2 changes: 1 addition & 1 deletion apps/blunt_absinthe/test/support/get_person.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ defmodule Blunt.Absinthe.Test.GetPerson do

field :error_out, :boolean, default: false

binding :person, BluntBoundedContext.QueryTest.ReadModel.Person
binding :person, Blunt.Absinthe.Test.ReadModel.Person
end
11 changes: 11 additions & 0 deletions apps/blunt_absinthe/test/support/read_model.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,23 @@ defmodule Blunt.Absinthe.Test.ReadModel do
schema "people" do
field :name, :string
field :gender, Ecto.Enum, values: @genders, default: :not_sure

embeds_one :address, Address, primary_key: false do
field :line1, :string
field :line2, :string
end
end

def changeset(person \\ %__MODULE__{}, attrs) do
person
|> Ecto.Changeset.cast(attrs, [:id, :name, :gender])
|> Ecto.Changeset.validate_required([:id, :gender])
|> Ecto.Changeset.cast_embed(:address, with: &address_changeset/2)
end

def address_changeset(address, attrs) do
address
|> Ecto.Changeset.cast(attrs, [:line1, :line2])
end
end
end
9 changes: 6 additions & 3 deletions apps/blunt_absinthe/test/support/schema_types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ defmodule Blunt.Absinthe.Test.SchemaTypes do
use Blunt.Absinthe
use Absinthe.Schema.Notation

alias Blunt.Absinthe.Test.{CreatePerson, GetPerson, UpdatePerson, Dog}
alias Blunt.Absinthe.Test.{CreatePerson, GetPerson, UpdatePerson, Address, Dog}

derive_enum :gender, {CreatePerson, :gender}

object :person do
field :id, :id
field :name, :string
field :gender, :gender
field :address, :address
end

derive_object(:dog, Dog)
derive_object(:address, Address)
derive_mutation_input Address

object :person_queries do
derive_query GetPerson, :person,
Expand All @@ -21,10 +24,10 @@ defmodule Blunt.Absinthe.Test.SchemaTypes do
]
end

derive_mutation_input(UpdatePerson, arg_types: [gender: :gender])
derive_mutation_input(UpdatePerson, arg_types: [gender: :gender, address: :address_input])

object :person_mutations do
derive_mutation CreatePerson, :person, arg_types: [gender: :gender]
derive_mutation CreatePerson, :person, arg_types: [gender: :gender, address: :address_input]
derive_mutation UpdatePerson, :person, input_object: true
end
end
1 change: 1 addition & 0 deletions apps/blunt_absinthe/test/support/update_person.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ defmodule Blunt.Absinthe.Test.UpdatePerson do
field :id, :binary_id
field :name, :string
field :gender, :enum, values: Person.genders(), required: false
field :address, Blunt.Absinthe.Test.Address, required: false
end