improvement: better tagged union handling

This commit is contained in:
Zach Daniel 2023-02-16 23:33:54 -05:00
parent f081126617
commit f1d4c1a3cb
5 changed files with 205 additions and 15 deletions

View file

@ -314,9 +314,9 @@ defmodule AshGraphql.Graphql.Resolver do
Enum.find(action_arguments, &(&1.name == key)) || Enum.find(attributes, &(&1.name == key))
if argument do
%{type: type, name: name} = argument
%{type: type, name: name, constraints: constraints} = argument
case handle_argument(type, value, name) do
case handle_argument(type, constraints, value, name) do
{:ok, value} ->
{:cont, {:ok, Map.put(arguments, name, value)}}
@ -329,15 +329,81 @@ defmodule AshGraphql.Graphql.Resolver do
end)
end
defp handle_argument(type, value, name) do
if union_type?(type) do
handle_union_type(value, name)
else
{:ok, value}
defp handle_argument({:array, type}, constraints, value, name) when is_list(value) do
value
|> Enum.reduce_while({:ok, []}, fn value, {:ok, acc} ->
case handle_argument(type, constraints[:items], value, name) do
{:ok, value} ->
{:cont, {:ok, [value | acc]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, value} -> {:ok, Enum.reverse(value)}
{:error, error} -> {:error, error}
end
end
defp handle_union_type(value, name) do
defp handle_argument(Ash.Type.Union, constraints, value, name) do
handle_union_type(value, constraints, name)
end
defp handle_argument(type, constraints, value, name) do
if Ash.Type.embedded_type?(type) and is_map(value) do
create_action =
if constraints[:create_action] do
Ash.Resource.Info.action(type, constraints[:create_action]) ||
Ash.Resource.Info.primary_action(type, :create)
else
Ash.Resource.Info.primary_action(type, :create)
end
update_action =
if constraints[:update_action] do
Ash.Resource.Info.action(type, constraints[:update_action]) ||
Ash.Resource.Info.primary_action(type, :update)
else
Ash.Resource.Info.primary_action(type, :update)
end
attributes = Ash.Resource.Info.public_attributes(type)
fields =
cond do
create_action && update_action ->
create_action.arguments ++ update_action.arguments ++ attributes
update_action ->
update_action.arguments ++ attributes
create_action ->
create_action.arguments ++ attributes
true ->
attributes
end
{:ok,
Map.new(value, fn {key, value} ->
field =
Enum.find(fields, fn field ->
field.name == key
end)
if field do
{key, handle_argument(field.type, field.constraints, value, "#{name}.#{key}")}
else
{key, value}
end
end)}
end
{:ok, value}
end
defp handle_union_type(value, constraints, name) do
value
|> Enum.reject(fn {_key, value} ->
is_nil(value)
@ -346,8 +412,14 @@ defmodule AshGraphql.Graphql.Resolver do
[] ->
{:ok, nil}
[{_key, value}] ->
{:ok, value}
[{key, value}] ->
config = constraints[:types][key]
if config[:tag] && is_map(value) do
{:ok, Map.put_new(value, config[:tag], config[:tag_value])}
else
{:ok, value}
end
key_vals ->
keys = Enum.map_join(key_vals, ", ", fn {key, _} -> to_string(key) end)
@ -357,10 +429,6 @@ defmodule AshGraphql.Graphql.Resolver do
end
end
defp union_type?({:array, type}), do: union_type?(type)
defp union_type?(Ash.Type.Union), do: true
defp union_type?(_), do: false
def validate_resolve_opts(resolution, pagination, opts, args) do
with page_opts <-
args

View file

@ -3100,7 +3100,7 @@ defmodule AshGraphql.Resource do
AshGraphql.Resource.Info.type(type)
else
if Ash.Type.embedded_type?(type) do
if input? do
if input? && type(type) do
:"#{AshGraphql.Resource.Info.type(resource)}_#{attribute.name}_input"
else
case type(type) do

View file

@ -146,6 +146,64 @@ defmodule AshGraphql.CreateTest do
} = result
end
test "an embedded union type can be written to" do
resp =
"""
mutation SimpleCreatePost($input: SimpleCreatePostInput) {
simpleCreatePost(input: $input) {
result{
text1
embedUnion {
... on PostEmbedUnionFoo {
value {
foo
}
}
... on PostEmbedUnionBar {
value {
bar
}
}
}
}
errors{
message
}
}
}
"""
|> Absinthe.run(AshGraphql.Test.Schema,
variables: %{
"input" => %{
"text1" => "foo",
"embedUnion" => %{
"foo" => %{
"foo" => "10"
}
}
}
}
)
assert {:ok, result} = resp
refute Map.has_key?(result, :errors)
assert %{
data: %{
"simpleCreatePost" => %{
"result" => %{
"embedUnion" => %{
"value" => %{
"foo" => "10"
}
}
}
}
}
} = result
end
test "a create can load a calculation without selecting the fields the calculation needs" do
resp =
"""

47
test/support/embeds.ex Normal file
View file

@ -0,0 +1,47 @@
defmodule Foo do
@moduledoc false
use Ash.Resource,
data_layer: :embedded,
extensions: [
AshGraphql.Resource
]
graphql do
type :foo_embed
end
attributes do
attribute :type, :atom do
constraints(one_of: [:foo])
writable?(false)
end
attribute :foo, :string do
allow_nil? false
end
end
end
defmodule Bar do
@moduledoc false
use Ash.Resource,
data_layer: :embedded,
extensions: [
AshGraphql.Resource
]
graphql do
type :bar_embed
end
attributes do
attribute :type, :atom do
constraints(one_of: [:foo])
writable?(false)
end
attribute :bar, :string do
allow_nil? false
end
end
end

View file

@ -249,6 +249,23 @@ defmodule AshGraphql.Test.Post do
]
)
attribute(:embed_union, :union,
constraints: [
types: [
foo: [
type: Foo,
tag: :type,
tag_value: :foo
],
bar: [
type: Bar,
tag: :type,
tag_value: :bar
]
]
]
)
create_timestamp(:created_at, private?: false)
end