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 case Map.fetch(args, :sort) do
{:ok, sort} -> {:ok, sort} ->
keyword_sort = keyword_sort =
Enum.map(sort, fn %{order: order, field: field} -> Enum.map(sort, fn %{order: order, field: field} = input ->
{field, order} 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) end)
fields = Ash.Query.sort(query, keyword_sort)
keyword_sort
|> Keyword.keys()
|> Enum.filter(&Ash.Resource.Info.public_aggregate(resource, &1))
query
|> Ash.Query.load(fields)
|> Ash.Query.sort(keyword_sort)
_ -> _ ->
query query

View file

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

View file

@ -269,6 +269,37 @@ defmodule AshGraphql.ReadTest do
assert %{data: %{"postLibrary" => [%{"foo" => "1foo2", "bar" => "1bar2"}]}} = result assert %{data: %{"postLibrary" => [%{"foo" => "1foo2", "bar" => "1bar2"}]}} = result
end 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 test "a read with a non-id primary key fills in the id field" do
record = record =
AshGraphql.Test.NonIdPrimaryKey AshGraphql.Test.NonIdPrimaryKey