Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix non-null-list-of-non-null error when root data is nil #1259

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Bugfix: [Handle non_null(list_of(:thing)) with null list elements properly](https://github.com/absinthe-graphql/absinthe/pull/1259)

## 1.7.4

- Feature: Support Dataloader 2.0
Expand Down
20 changes: 15 additions & 5 deletions lib/absinthe/phase/document/execution/resolution.ex
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,21 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do
|> propagate_null_trimming
end

defp maybe_add_non_null_error([], values, %Type.NonNull{of_type: %Type.List{}}) do
defp maybe_add_non_null_error([], nil, %Type.NonNull{}) do
Copy link
Contributor

Choose a reason for hiding this comment

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

Moved this up as this the most fundamental check, best to do it first.

["Cannot return null for non-nullable field"]
end

# Unwrap the non null so we can check again for the possible case of a non null
# inside of a list. We want that clause to handle both
# - `non_null(list_of(non_null(:thing)))`
# - `list_of(non_null(:thing))`
# Thus the single layer of unwrapping here.
defp maybe_add_non_null_error([], values, %Type.NonNull{of_type: type}) do
maybe_add_non_null_error([], values, type)
end

defp maybe_add_non_null_error([], values, %Type.List{of_type: %Type.NonNull{}})
Copy link
Contributor

Choose a reason for hiding this comment

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

Before, this clause would happen for any non_null(list_of( regardless of wether the inner type was non_null or not. Now we check the inner type properly.

when is_list(values) do
values
|> Enum.with_index()
|> Enum.filter(&is_nil(elem(&1, 0)))
Expand All @@ -294,10 +308,6 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do
end)
end

defp maybe_add_non_null_error([], nil, %Type.NonNull{}) do
["Cannot return null for non-nullable field"]
end

defp maybe_add_non_null_error(errors, _, _) do
errors
end
Expand Down
98 changes: 72 additions & 26 deletions test/absinthe/phase/execution/non_null_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
defmodule Schema do
use Absinthe.Schema

defp thing_resolver(_, %{make_null: make_null}, _) do
if make_null do
defp thing_resolver(_, %{return_null: return_null}, _) do
if return_null do
{:ok, nil}
else
{:ok, %{}}
Expand All @@ -16,21 +16,29 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
{:ok, %{}}
end

defp things_resolver(_, %{make_null: make_null}, _) do
if make_null do
defp things_resolver(_, %{return_null_element: return_null_element}, _) do
if return_null_element do
{:ok, [nil]}
else
{:ok, [%{}]}
end
end

defp things_resolver(_, %{return_null: return_null}, _) do
if return_null do
{:ok, nil}
else
{:ok, [%{}]}
end
end

defp things_resolver(_, _, _) do
{:ok, [%{}]}
end

object :thing do
field :nullable, :thing do
arg :make_null, :boolean
arg :return_null, :boolean
resolve &thing_resolver/3
end

Expand All @@ -41,7 +49,7 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
testing the null handling behaviour.
"""
field :non_null, non_null(:thing) do
arg :make_null, :boolean
arg :return_null, :boolean
resolve &thing_resolver/3
end

Expand All @@ -52,14 +60,14 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
end

field :non_null_list_of_non_null, non_null(list_of(non_null(:thing))) do
arg :make_null, :boolean
arg :return_null_element, :boolean
resolve &things_resolver/3
end
end

query do
field :nullable, :thing do
arg :make_null, :boolean
arg :return_null, :boolean
resolve &thing_resolver/3
end

Expand All @@ -78,7 +86,13 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
end

field :non_null_list_of_non_null, non_null(list_of(non_null(:thing))) do
arg :make_null, :boolean
arg :return_null_element, :boolean
resolve &things_resolver/3
end

field :non_null_list_of_nullable, non_null(list_of(:thing)) do
arg :return_null, :boolean
arg :return_null_element, :boolean
resolve &things_resolver/3
end

Expand All @@ -89,7 +103,7 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
testing the null handling behaviour.
"""
field :non_null, non_null(:thing) do
arg :make_null, :boolean
arg :return_null, :boolean
resolve &thing_resolver/3
end
end
Expand All @@ -98,17 +112,17 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
test "getting a null value normally works fine" do
doc = """
{
nullable { nullable(makeNull: true) { __typename }}
nullable { nullable(returnNull: true) { __typename }}
}
"""

assert {:ok, %{data: %{"nullable" => %{"nullable" => nil}}}} == Absinthe.run(doc, Schema)
end

test "returning nil from a non null field makes the parent nullable null" do
test "returning nil from a non null field returns an error and makes the parent nullable null" do
doc = """
{
nullable { nullable { nonNull(makeNull: true) { __typename }}}
nullable { nullable { nonNull(returnNull: true) { __typename }}}
}
"""

Expand All @@ -125,10 +139,10 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
end

test "returning nil from a non null child of non nulls pushes nil all the way up to data" do
test "returning nil from a non null child of non nulls returns an error and pushes nil all the way up to data" do
doc = """
{
nonNull { nonNull { nonNull(makeNull: true) { __typename }}}
nonNull { nonNull { nonNull(returnNull: true) { __typename }}}
}
"""

Expand All @@ -145,7 +159,7 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
end

test "error propagation to root field returns nil on data" do
test "returning an error from a non null field makes the parent nullable null" do
doc = """
{
nullable { nullable { nonNullErrorField }}
Expand All @@ -165,7 +179,7 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
end

test "returning an error from a non null field makes the parent nullable null" do
test "error propagation to root field returns nil on data" do
doc = """
{
nonNull { nonNull { nonNullErrorField }}
Expand Down Expand Up @@ -203,10 +217,10 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
end

describe "lists" do
test "list of nullable things works when child has a null violation" do
test "list of nullable things returns an error when child has a null violation" do
doc = """
{
nullableListOfNullable { nonNull(makeNull: true) { __typename } }
nullableListOfNullable { nonNull(returnNull: true) { __typename } }
}
"""

Expand All @@ -223,10 +237,10 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
end

test "list of non null things works when child has a null violation" do
test "list of non null things returns an error when child has a null violation" do
doc = """
{
nullableListOfNonNull { nonNull(makeNull: true) { __typename } }
nullableListOfNonNull { nonNull(returnNull: true) { __typename } }
}
"""

Expand All @@ -243,10 +257,10 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
end

test "list of non null things works when child has a null violation and the root field is non null" do
test "list of non null things returns an error when child has a null violation and the root field is non null" do
doc = """
{
nonNullListOfNonNull { nonNull(makeNull: true) { __typename } }
nonNullListOfNonNull { nonNull(returnNull: true) { __typename } }
}
"""

Expand All @@ -263,10 +277,42 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
end

test "list of non null things works when child is null" do
test "non null list of nullable returns an error when null" do
doc = """
{
nonNullListOfNullable(returnNull: true) { __typename }
}
"""

data = nil

errors = [
%{
locations: [%{column: 3, line: 2}],
message: "Cannot return null for non-nullable field",
path: ["nonNullListOfNullable"]
}
]

assert {:ok, %{data: data, errors: errors}} == Absinthe.run(doc, Schema)
benwilson512 marked this conversation as resolved.
Show resolved Hide resolved
end

test "non null list of nullable allows returning a list with null elements" do
doc = """
{
nonNullListOfNullable(returnNullElement: true) { __typename }
}
"""

data = %{"nonNullListOfNullable" => [nil]}

assert {:ok, %{data: data}} == Absinthe.run(doc, Schema)
end
benwilson512 marked this conversation as resolved.
Show resolved Hide resolved

test "list of non null things returns an error when child is null" do
doc = """
{
nonNullListOfNonNull(makeNull: true) { __typename }
nonNullListOfNonNull(returnNullElement: true) { __typename }
}
"""

Expand All @@ -290,7 +336,7 @@ defmodule Absinthe.Phase.Document.Execution.NonNullTest do
nonNullListOfNonNull {
nonNullListOfNonNull {
nonNullListOfNonNull {
nonNullListOfNonNull(makeNull: true) { __typename }
nonNullListOfNonNull(returnNullElement: true) { __typename }
}
}
}
Expand Down