improvement: support calculation sort input

closes #31
This commit is contained in:
Zach Daniel 2022-10-12 21:15:42 -04:00
parent 3a11d988ab
commit 9cc9da0f2e
3 changed files with 163 additions and 83 deletions

View file

@ -788,18 +788,19 @@ defmodule AshGraphql.Graphql.Resolver do
case Map.fetch(args, :sort) do
{:ok, sort} ->
keyword_sort =
Enum.map(sort, fn %{order: order, field: field} ->
Enum.map(sort, fn %{order: order, field: field} = input ->
case Ash.Resource.Info.calculation(resource, field) do
%{arguments: [_ | _]} ->
input_name = String.to_existing_atom("#{field}_input")
{field, {order, input[input_name] || %{}}}
_ ->
{field, order}
end
end)
fields =
keyword_sort
|> Keyword.keys()
|> Enum.filter(&Ash.Resource.Info.public_aggregate(resource, &1))
query
|> Ash.Query.load(fields)
|> Ash.Query.sort(keyword_sort)
Ash.Query.sort(query, keyword_sort)
_ ->
query

View file

@ -789,7 +789,7 @@ defmodule AshGraphql.Resource do
|> maybe_wrap_non_null(argument_required?(argument))
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: argument.name,
identifier: name,
module: schema,
name: to_string(name),
type: type,
@ -1788,7 +1788,8 @@ defmodule AshGraphql.Resource do
_ ->
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: [
fields:
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :order,
module: schema,
@ -1806,7 +1807,7 @@ defmodule AshGraphql.Resource do
},
__reference__: ref(__ENV__)
}
],
] ++ calc_input_fields(resource, schema),
identifier: resource_sort_type(resource),
module: schema,
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize(),
@ -1818,6 +1819,43 @@ defmodule AshGraphql.Resource do
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp calc_input_fields(resource, schema) do
calcs =
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.reject(fn
%{type: {:array, _}} ->
true
calc ->
Ash.Type.embedded_type?(calc.type) || Enum.empty?(calc.arguments)
end)
field_names = AshGraphql.Resource.Info.field_names(resource)
Enum.map(calcs, fn calc ->
input_name = "#{field_names[calc.name] || calc.name}_input"
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: String.to_atom("#{calc.name}_input"),
module: schema,
name: input_name,
type: calc_input_type(calc.name, resource),
__reference__: ref(__ENV__)
}
end)
end
# sobelow_skip ["DOS.StringToAtom"]
defp calc_input_type(calc, resource) do
field_names = AshGraphql.Resource.Info.field_names(resource)
String.to_atom(
"#{AshGraphql.Resource.Info.type(resource)}_#{field_names[calc] || calc}_input"
)
end
defp filter_input(resource, schema) do
case resource_filter_fields(resource, schema) do
[] ->
@ -1838,11 +1876,9 @@ defmodule AshGraphql.Resource do
defp calculation_input(resource, schema) do
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.filter(fn %{calculation: {module, _}} ->
|> Enum.flat_map(fn %{calculation: {module, _}} = calculation ->
Code.ensure_compiled(module)
:erlang.function_exported(module, :expression, 2)
end)
|> Enum.flat_map(fn calculation ->
filterable? = :erlang.function_exported(module, :expression, 2)
field_type = calculation_type(calculation, resource)
arguments = calculation_args(calculation, resource, schema)
@ -1864,56 +1900,54 @@ defmodule AshGraphql.Resource do
)
)
filter_input = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
input = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: arguments,
identifier:
String.to_atom(
to_string(calculation_filter_field_type(resource, calculation)) <> "_input"
),
identifier: String.to_atom(to_string(calc_input_type(calculation.name, resource))),
module: schema,
name:
Macro.camelize(
to_string(calculation_filter_field_type(resource, calculation)) <> "_input"
),
name: Macro.camelize(to_string(calc_input_type(calculation.name, resource))),
__reference__: ref(__ENV__)
}
types =
if Enum.empty?(arguments) do
[]
else
[input]
end
if filterable? do
type_def =
if Enum.empty?(arguments) do
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: filter_fields,
identifier: calculation_filter_field_type(resource, calculation),
module: schema,
name:
Macro.camelize(to_string(calculation_filter_field_type(resource, calculation))),
__reference__: ref(__ENV__)
}
else
filter_input_field = %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :input,
module: schema,
name: "input",
type:
String.to_atom(
to_string(calculation_filter_field_type(resource, calculation)) <> "_input"
),
type: String.to_atom(to_string(calc_input_type(calculation.name, resource))),
__reference__: ref(__ENV__)
}
if Enum.empty?(arguments) do
type_def = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: filter_fields,
identifier: calculation_filter_field_type(resource, calculation),
module: schema,
name: Macro.camelize(to_string(calculation_filter_field_type(resource, calculation))),
__reference__: ref(__ENV__)
}
[
type_def
]
else
type_def = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: [filter_input_field | filter_fields],
identifier: calculation_filter_field_type(resource, calculation),
module: schema,
name: Macro.camelize(to_string(calculation_filter_field_type(resource, calculation))),
name:
Macro.camelize(to_string(calculation_filter_field_type(resource, calculation))),
__reference__: ref(__ENV__)
}
end
[
filter_input,
type_def
]
[type_def | types]
else
types
end
end)
end
@ -2091,12 +2125,12 @@ defmodule AshGraphql.Resource do
identifier: resource_sort_field_type(resource),
__reference__: ref(__ENV__),
values:
Enum.map(sort_values, fn sort_value ->
Enum.map(sort_values, fn {sort_value_alias, sort_value} ->
%Absinthe.Blueprint.Schema.EnumValueDefinition{
module: schema,
identifier: sort_value,
identifier: sort_value_alias,
__reference__: AshGraphql.Resource.ref(env),
name: String.upcase(to_string(sort_value)),
name: String.upcase(to_string(sort_value_alias)),
value: sort_value
}
end)
@ -2164,12 +2198,25 @@ defmodule AshGraphql.Resource do
end)
|> Enum.map(& &1.name)
calculation_sort_values =
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.reject(fn
%{type: {:array, _}} ->
true
attribute ->
Ash.Type.embedded_type?(attribute.type)
end)
|> Enum.map(& &1.name)
attribute_sort_values
|> Enum.concat(aggregate_sort_values)
|> Enum.map(fn name ->
field_names[name] || name
end)
|> Enum.concat(calculation_sort_values)
|> Enum.uniq()
|> Enum.map(fn name ->
{field_names[name] || name, name}
end)
end
# sobelow_skip ["DOS.StringToAtom"]
@ -2687,7 +2734,7 @@ defmodule AshGraphql.Resource do
raise "Cannot construct an input type for #{inspect(type)}"
end
AshGraphql.Resource.Info.type(resource)
AshGraphql.Resource.Info.type(type)
else
if Ash.Type.embedded_type?(type) do
if input? do
@ -2733,7 +2780,8 @@ defmodule AshGraphql.Resource do
Ash.Type.Atom,
%Ash.Resource.Attribute{constraints: constraints, name: name},
resource
) do
)
when resource do
if is_list(constraints[:one_of]) do
atom_enum_type(resource, name)
else

View file

@ -269,6 +269,37 @@ defmodule AshGraphql.ReadTest do
assert %{data: %{"postLibrary" => [%{"foo" => "1foo2", "bar" => "1bar2"}]}} = result
end
test "the same calculation can be sorted on twice with different arguments via aliases" do
AshGraphql.Test.Post
|> Ash.Changeset.for_create(:create, text: "bar", text1: "1", text2: "2", published: true)
|> AshGraphql.Test.Api.create!()
AshGraphql.Test.Post
|> Ash.Changeset.for_create(:create, text: "bar", text1: "1", text2: "2", published: true)
|> AshGraphql.Test.Api.create!()
resp =
"""
query PostLibrary($published: Boolean) {
postLibrary(published: $published, sort: [{field: TEXT1_AND2, order: DESC, text1And2Input: {separator: "a"}}, {field: TEXT1_AND2, order: DESC, text1And2Input: {separator: "b"}}]) {
a: text1And2(separator: "a")
b: text1And2(separator: "b")
}
}
"""
|> Absinthe.run(AshGraphql.Test.Schema)
assert {:ok, result} = resp
refute Map.has_key?(result, :errors)
assert %{
data: %{
"postLibrary" => [%{"a" => "1a2", "b" => "1b2"}, %{"a" => "1a2", "b" => "1b2"}]
}
} = result
end
test "a read with a non-id primary key fills in the id field" do
record =
AshGraphql.Test.NonIdPrimaryKey