mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
improvement: support calculation sorts
This commit is contained in:
parent
bed9286c6c
commit
d45a9dbbfa
13 changed files with 300 additions and 69 deletions
|
@ -293,8 +293,19 @@ defmodule Ash.Actions.Read do
|
||||||
query.filter
|
query.filter
|
||||||
) do
|
) do
|
||||||
{:ok, filter} ->
|
{:ok, filter} ->
|
||||||
{:ok, %{query | filter: filter},
|
case Ash.Actions.Sort.process(
|
||||||
%{requests: load_requests, notifications: before_notifications}}
|
query.resource,
|
||||||
|
query.sort,
|
||||||
|
query.aggregates,
|
||||||
|
query.context
|
||||||
|
) do
|
||||||
|
{:ok, sort} ->
|
||||||
|
{:ok, %{query | filter: filter, sort: sort},
|
||||||
|
%{requests: load_requests, notifications: before_notifications}}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
|
@ -942,26 +953,30 @@ defmodule Ash.Actions.Read do
|
||||||
defp add_calculations(data_layer_query, query, calculations_to_add) do
|
defp add_calculations(data_layer_query, query, calculations_to_add) do
|
||||||
Enum.reduce_while(calculations_to_add, {:ok, data_layer_query}, fn calculation,
|
Enum.reduce_while(calculations_to_add, {:ok, data_layer_query}, fn calculation,
|
||||||
{:ok, data_layer_query} ->
|
{:ok, data_layer_query} ->
|
||||||
expression = calculation.module.expression(calculation.opts, calculation.context)
|
if Ash.DataLayer.data_layer_can?(query.resource, :expression_calculation) do
|
||||||
|
expression = calculation.module.expression(calculation.opts, calculation.context)
|
||||||
|
|
||||||
with {:ok, expression} <-
|
with {:ok, expression} <-
|
||||||
Ash.Filter.hydrate_refs(expression, %{
|
Ash.Filter.hydrate_refs(expression, %{
|
||||||
resource: query.resource,
|
resource: query.resource,
|
||||||
aggregates: query.aggregates,
|
aggregates: query.aggregates,
|
||||||
calculations: query.calculations,
|
calculations: query.calculations,
|
||||||
public?: false
|
public?: false
|
||||||
}),
|
}),
|
||||||
{:ok, query} <-
|
{:ok, query} <-
|
||||||
Ash.DataLayer.add_calculation(
|
Ash.DataLayer.add_calculation(
|
||||||
data_layer_query,
|
data_layer_query,
|
||||||
calculation,
|
calculation,
|
||||||
expression,
|
expression,
|
||||||
query.resource
|
query.resource
|
||||||
) do
|
) do
|
||||||
{:cont, {:ok, query}}
|
{:cont, {:ok, query}}
|
||||||
|
else
|
||||||
|
other ->
|
||||||
|
{:halt, other}
|
||||||
|
end
|
||||||
else
|
else
|
||||||
other ->
|
{:halt, {:error, "Expression calculations are not supported"}}
|
||||||
{:halt, other}
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,11 +9,60 @@ defmodule Ash.Actions.Sort do
|
||||||
|
|
||||||
@sort_orders [:asc, :desc, :asc_nils_first, :asc_nils_last, :desc_nils_first, :desc_nils_last]
|
@sort_orders [:asc, :desc, :asc_nils_first, :asc_nils_last, :desc_nils_first, :desc_nils_last]
|
||||||
|
|
||||||
def process(_resource, empty, _aggregates) when empty in [nil, []], do: {:ok, []}
|
def process(_resource, empty, _aggregates, context \\ %{})
|
||||||
|
|
||||||
def process(resource, sort, aggregates) when is_list(sort) do
|
def process(_resource, empty, _aggregates, _context) when empty in [nil, []], do: {:ok, []}
|
||||||
|
|
||||||
|
def process(resource, sort, aggregates, context) when is_list(sort) do
|
||||||
sort
|
sort
|
||||||
|
|> Enum.map(fn {key, val} ->
|
||||||
|
if !is_atom(val) do
|
||||||
|
{key, {:asc, val}}
|
||||||
|
else
|
||||||
|
{key, val}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|> Enum.reduce({[], []}, fn
|
|> Enum.reduce({[], []}, fn
|
||||||
|
{field, {inner_order, _} = order}, {sorts, errors} when inner_order in @sort_orders ->
|
||||||
|
case Ash.Resource.Info.calculation(resource, field) do
|
||||||
|
nil ->
|
||||||
|
{sorts,
|
||||||
|
[
|
||||||
|
"Cannot provide context to a non-calculation field while sorting"
|
||||||
|
| errors
|
||||||
|
]}
|
||||||
|
|
||||||
|
calc ->
|
||||||
|
{module, opts} = calc.calculation
|
||||||
|
|
||||||
|
if :erlang.function_exported(module, :expression, 2) do
|
||||||
|
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation_sort) do
|
||||||
|
calculation_sort(
|
||||||
|
field,
|
||||||
|
calc,
|
||||||
|
module,
|
||||||
|
opts,
|
||||||
|
calc.type,
|
||||||
|
order,
|
||||||
|
sorts,
|
||||||
|
errors,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
else
|
||||||
|
{sorts, ["Datalayer cannot sort on calculations"]}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{sorts, ["Calculations cannot be sorted on unless they define an expression"]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{%Ash.Query.Calculation{} = calc, order}, {sorts, errors} ->
|
||||||
|
if order in @sort_orders do
|
||||||
|
{sorts ++ [{calc, order}], errors}
|
||||||
|
else
|
||||||
|
{sorts, [InvalidSortOrder.exception(order: order) | errors]}
|
||||||
|
end
|
||||||
|
|
||||||
{field, order}, {sorts, errors} when order in @sort_orders ->
|
{field, order}, {sorts, errors} when order in @sort_orders ->
|
||||||
attribute = Ash.Resource.Info.attribute(resource, field)
|
attribute = Ash.Resource.Info.attribute(resource, field)
|
||||||
|
|
||||||
|
@ -21,6 +70,29 @@ defmodule Ash.Actions.Sort do
|
||||||
Map.has_key?(aggregates, field) ->
|
Map.has_key?(aggregates, field) ->
|
||||||
aggregate_sort(aggregates, field, order, resource, sorts, errors)
|
aggregate_sort(aggregates, field, order, resource, sorts, errors)
|
||||||
|
|
||||||
|
calc = Ash.Resource.Info.calculation(resource, field) ->
|
||||||
|
{module, opts} = calc.calculation
|
||||||
|
|
||||||
|
if :erlang.function_exported(module, :expression, 2) do
|
||||||
|
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation_sort) do
|
||||||
|
calculation_sort(
|
||||||
|
field,
|
||||||
|
calc,
|
||||||
|
module,
|
||||||
|
opts,
|
||||||
|
calc.type,
|
||||||
|
order,
|
||||||
|
sorts,
|
||||||
|
errors,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
else
|
||||||
|
{sorts, ["Datalayer cannot sort on calculations"]}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{sorts, ["Calculations cannot be sorted on unless they define an expression"]}
|
||||||
|
end
|
||||||
|
|
||||||
!attribute ->
|
!attribute ->
|
||||||
{sorts, [NoSuchAttribute.exception(attribute: field) | errors]}
|
{sorts, [NoSuchAttribute.exception(attribute: field) | errors]}
|
||||||
|
|
||||||
|
@ -78,25 +150,93 @@ defmodule Ash.Actions.Sort do
|
||||||
) do
|
) do
|
||||||
{sorts ++ [{field, order}], errors}
|
{sorts ++ [{field, order}], errors}
|
||||||
else
|
else
|
||||||
{sorts, AggregatesNotSupported.exception(resource: resource, feature: "sorting")}
|
{sorts, [AggregatesNotSupported.exception(resource: resource, feature: "sorting") | errors]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp calculation_sort(field, calc, module, opts, type, order, sorts, errors, context) do
|
||||||
|
{order, calc_context} =
|
||||||
|
case order do
|
||||||
|
order when is_atom(order) ->
|
||||||
|
{order, %{}}
|
||||||
|
|
||||||
|
{order, value} when is_list(value) ->
|
||||||
|
{order, Map.new(value)}
|
||||||
|
|
||||||
|
{order, value} when is_map(value) ->
|
||||||
|
{order, value}
|
||||||
|
|
||||||
|
other ->
|
||||||
|
{other, %{}}
|
||||||
|
end
|
||||||
|
|
||||||
|
with {:ok, input} <- Ash.Query.validate_calculation_arguments(calc, calc_context),
|
||||||
|
{:ok, calc} <-
|
||||||
|
Ash.Query.Calculation.new(
|
||||||
|
field,
|
||||||
|
module,
|
||||||
|
opts,
|
||||||
|
type,
|
||||||
|
Map.put(input, :context, context)
|
||||||
|
) do
|
||||||
|
{sorts ++ [{calc, order}], errors}
|
||||||
|
else
|
||||||
|
{:error, error} ->
|
||||||
|
{sorts, [error | errors]}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def runtime_sort(results, empty) when empty in [nil, []], do: results
|
def runtime_sort(results, empty) when empty in [nil, []], do: results
|
||||||
|
|
||||||
def runtime_sort(results, [{field, direction}]) do
|
def runtime_sort([%resource{} | _] = results, [{field, direction}]) do
|
||||||
sort_by(results, &Map.get(&1, field), direction)
|
sort_by(results, &resolve_field(&1, field, resource), direction)
|
||||||
end
|
end
|
||||||
|
|
||||||
def runtime_sort(results, [{field, direction} | rest]) do
|
def runtime_sort([%resource{} | _] = results, [{field, direction} | rest]) do
|
||||||
results
|
results
|
||||||
|> Enum.group_by(&Map.get(&1, field))
|
|> Enum.group_by(&resolve_field(&1, field, resource))
|
||||||
|> sort_by(fn {key, _value} -> key end, direction)
|
|> sort_by(fn {key, _value} -> key end, direction)
|
||||||
|> Enum.flat_map(fn {_, records} ->
|
|> Enum.flat_map(fn {_, records} ->
|
||||||
runtime_sort(records, rest)
|
runtime_sort(records, rest)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp resolve_field(record, %Ash.Query.Calculation{} = calc, resource) do
|
||||||
|
cond do
|
||||||
|
:erlang.function_exported(calc.module, :calculate, 3) ->
|
||||||
|
calc.module.calculate([record], calc.opts, calc.context)
|
||||||
|
|
||||||
|
:erlang.function_exported(calc.module, :expression, 2) ->
|
||||||
|
expression = calc.module.expression(calc.opts, calc.context)
|
||||||
|
|
||||||
|
case Ash.Filter.hydrate_refs(expression, %{
|
||||||
|
resource: resource,
|
||||||
|
aggregates: %{},
|
||||||
|
calculations: %{},
|
||||||
|
public?: false
|
||||||
|
}) do
|
||||||
|
{:ok, expression} ->
|
||||||
|
case Ash.Filter.Runtime.do_match(record, expression) do
|
||||||
|
{:ok, value} ->
|
||||||
|
{:ok, value}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolve_field(record, field, _resource) do
|
||||||
|
Map.get(record, field)
|
||||||
|
end
|
||||||
|
|
||||||
# :asc/:desc added to elixir in 1.10. sort_by and to_sort_by_fun copied from core
|
# :asc/:desc added to elixir in 1.10. sort_by and to_sort_by_fun copied from core
|
||||||
defp sort_by(enumerable, mapper, sorter) do
|
defp sort_by(enumerable, mapper, sorter) do
|
||||||
enumerable
|
enumerable
|
||||||
|
@ -180,6 +320,10 @@ defmodule Ash.Actions.Sort do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp to_sort_by_fun({direction, _input}) do
|
||||||
|
to_sort_by_fun(direction)
|
||||||
|
end
|
||||||
|
|
||||||
defp to_sort_by_fun(module) when is_atom(module),
|
defp to_sort_by_fun(module) when is_atom(module),
|
||||||
do: &(module.compare(elem(&1, 1), elem(&2, 1)) != :gt)
|
do: &(module.compare(elem(&1, 1), elem(&2, 1)) != :gt)
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,8 @@ defmodule Ash.DataLayer.Ets do
|
||||||
end
|
end
|
||||||
|
|
||||||
def can?(_, :composite_primary_key), do: true
|
def can?(_, :composite_primary_key), do: true
|
||||||
|
def can?(_, :expression_calculation), do: true
|
||||||
|
def can?(_, :expression_calculation_sort), do: true
|
||||||
def can?(_, :multitenancy), do: true
|
def can?(_, :multitenancy), do: true
|
||||||
def can?(_, :upsert), do: true
|
def can?(_, :upsert), do: true
|
||||||
def can?(_, :create), do: true
|
def can?(_, :create), do: true
|
||||||
|
|
|
@ -2167,7 +2167,7 @@ defmodule Ash.Filter do
|
||||||
|
|
||||||
defp add_calculation_expression(context, nested_statement, field, module, expression) do
|
defp add_calculation_expression(context, nested_statement, field, module, expression) do
|
||||||
if Ash.DataLayer.data_layer_can?(context.resource, :expression_calculation) &&
|
if Ash.DataLayer.data_layer_can?(context.resource, :expression_calculation) &&
|
||||||
:erlang.function_exported(module, :expression, 1) do
|
:erlang.function_exported(module, :expression, 2) do
|
||||||
case parse_predicates(nested_statement, Map.get(context.calculations, field), context) do
|
case parse_predicates(nested_statement, Map.get(context.calculations, field), context) do
|
||||||
{:ok, nested_statement} ->
|
{:ok, nested_statement} ->
|
||||||
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
|
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
|
||||||
|
|
|
@ -1674,7 +1674,7 @@ defmodule Ash.Query do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_sort(%{resource: resource, sort: sort} = query) do
|
defp validate_sort(%{resource: resource, sort: sort} = query) do
|
||||||
case Sort.process(resource, sort, query.aggregates) do
|
case Sort.process(resource, sort, query.aggregates, query.context) do
|
||||||
{:ok, new_sort} -> %{query | sort: new_sort}
|
{:ok, new_sort} -> %{query | sort: new_sort}
|
||||||
{:error, error} -> add_error(query, :sort, error)
|
{:error, error} -> add_error(query, :sort, error)
|
||||||
end
|
end
|
||||||
|
|
|
@ -124,18 +124,12 @@ defmodule Ash.Sort do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_field(resource, field) do
|
defp get_field(resource, field) do
|
||||||
case Ash.Resource.Info.public_attribute(resource, field) do
|
with nil <- Ash.Resource.Info.public_attribute(resource, field),
|
||||||
%{name: name} ->
|
nil <- Ash.Resource.Info.public_aggregate(resource, field),
|
||||||
name
|
nil <- Ash.Resource.Info.public_calculation(resource, field) do
|
||||||
|
nil
|
||||||
nil ->
|
else
|
||||||
case Ash.Resource.Info.public_attribute(resource, field) do
|
%{name: name} -> name
|
||||||
%{name: name} ->
|
|
||||||
name
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Ash.Test.Actions.CreateTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
import Ash.Changeset
|
import Ash.Changeset
|
||||||
|
import Ash.Test.Helpers
|
||||||
|
|
||||||
defmodule Authorized do
|
defmodule Authorized do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
@ -465,24 +466,28 @@ defmodule Ash.Test.Actions.CreateTest do
|
||||||
|> new()
|
|> new()
|
||||||
|> change_attribute(:title, "title2")
|
|> change_attribute(:title, "title2")
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
post3 =
|
post3 =
|
||||||
Post
|
Post
|
||||||
|> new()
|
|> new()
|
||||||
|> change_attribute(:title, "title3")
|
|> change_attribute(:title, "title3")
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> new(%{title: "cannot_be_missing"})
|
|> new(%{title: "cannot_be_missing"})
|
||||||
|> replace_relationship(:related_posts, [post2, post3])
|
|> replace_relationship(:related_posts, [post2, post3])
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
assert Enum.sort(post.related_posts) ==
|
assert Enum.sort(post.related_posts) ==
|
||||||
Enum.sort([
|
Enum.sort([
|
||||||
Api.get!(Post, post2.id),
|
Api.get!(Post, post2.id),
|
||||||
Api.get!(Post, post3.id)
|
Api.get!(Post, post3.id)
|
||||||
])
|
])
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
import Ash.Changeset
|
import Ash.Changeset
|
||||||
|
import Ash.Test.Helpers
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
test "it returns a matching record", %{post: post} do
|
test "it returns a matching record", %{post: post} do
|
||||||
assert {:ok, fetched_post} = Api.get(Post, post.id)
|
assert {:ok, fetched_post} = Api.get(Post, post.id)
|
||||||
|
|
||||||
assert fetched_post == post
|
assert clear_meta(fetched_post) == post
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns nil when there is no matching record" do
|
test "it returns nil when there is no matching record" do
|
||||||
|
@ -101,7 +102,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
test "it uses identities if they exist", %{post: post} do
|
test "it uses identities if they exist", %{post: post} do
|
||||||
assert {:ok, fetched_post} = Api.get(Post, uuid: post.uuid)
|
assert {:ok, fetched_post} = Api.get(Post, uuid: post.uuid)
|
||||||
|
|
||||||
assert fetched_post == post
|
assert clear_meta(fetched_post) == post
|
||||||
end
|
end
|
||||||
|
|
||||||
test "raises an error when the first argument is not a module" do
|
test "raises an error when the first argument is not a module" do
|
||||||
|
@ -140,7 +141,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns a matching record", %{post: post} do
|
test "it returns a matching record", %{post: post} do
|
||||||
assert ^post = Api.get!(Post, post.id)
|
assert ^post = clear_meta(Api.get!(Post, post.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "raises an error when the first argument is not a module", %{post: post} do
|
test "raises an error when the first argument is not a module", %{post: post} do
|
||||||
|
@ -392,6 +393,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == ^post1.title)
|
|> Ash.Query.filter(title == ^post1.title)
|
||||||
|> Api.read()
|
|> Api.read()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a filter returns multiple records if they match", %{post1: post1, post2: post2} do
|
test "a filter returns multiple records if they match", %{post1: post1, post2: post2} do
|
||||||
|
@ -399,6 +401,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(contents == "yeet")
|
|> Ash.Query.filter(contents == "yeet")
|
||||||
|> Api.read()
|
|> Api.read()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
assert post1 in results
|
assert post1 in results
|
||||||
assert post2 in results
|
assert post2 in results
|
||||||
|
@ -495,6 +498,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :asc)
|
|> Ash.Query.sort(title: :asc)
|
||||||
|> Api.read()
|
|> Api.read()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a sort will sor rows accordingly when descending", %{
|
test "a sort will sor rows accordingly when descending", %{
|
||||||
|
@ -505,6 +509,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :desc)
|
|> Ash.Query.sort(title: :desc)
|
||||||
|> Api.read()
|
|> Api.read()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a nested sort sorts accordingly", %{post1: post1, post2: post2} do
|
test "a nested sort sorts accordingly", %{post1: post1, post2: post2} do
|
||||||
|
@ -512,11 +517,13 @@ defmodule Ash.Test.Actions.ReadTest do
|
||||||
Post
|
Post
|
||||||
|> new(%{title: "abc", contents: "xyz"})
|
|> new(%{title: "abc", contents: "xyz"})
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
assert {:ok, [^post1, ^middle_post, ^post2]} =
|
assert {:ok, [^post1, ^middle_post, ^post2]} =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :asc, contents: :asc)
|
|> Ash.Query.sort(title: :asc, contents: :asc)
|
||||||
|> Api.read()
|
|> Api.read()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Ash.Test.Actions.UpdateTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
import Ash.Changeset
|
import Ash.Changeset
|
||||||
|
import Ash.Test.Helpers
|
||||||
|
|
||||||
defmodule Authorized do
|
defmodule Authorized do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
@ -372,6 +373,7 @@ defmodule Ash.Test.Actions.UpdateTest do
|
||||||
Api.get!(Post, post2.id),
|
Api.get!(Post, post2.id),
|
||||||
Api.get!(Post, post3.id)
|
Api.get!(Post, post3.id)
|
||||||
])
|
])
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it updates any join fields" do
|
test "it updates any join fields" do
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule Ash.DataLayer.EtsTest do
|
defmodule Ash.DataLayer.EtsTest do
|
||||||
use ExUnit.Case, async: false
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
import Ash.Test.Helpers
|
||||||
|
|
||||||
alias Ash.DataLayer.Ets, as: EtsDataLayer
|
alias Ash.DataLayer.Ets, as: EtsDataLayer
|
||||||
alias Ash.DataLayer.Ets.Query
|
alias Ash.DataLayer.Ets.Query
|
||||||
|
|
||||||
|
@ -116,7 +118,7 @@ defmodule Ash.DataLayer.EtsTest do
|
||||||
|> Ash.Query.new()
|
|> Ash.Query.new()
|
||||||
|> Ash.Query.sort(:name)
|
|> Ash.Query.sort(:name)
|
||||||
|
|
||||||
assert [^joe, ^matthew, ^mike, ^zachary] = EtsApiTest.read!(query)
|
assert [^joe, ^matthew, ^mike, ^zachary] = clear_meta(EtsApiTest.read!(query))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "limit" do
|
test "limit" do
|
||||||
|
@ -131,7 +133,7 @@ defmodule Ash.DataLayer.EtsTest do
|
||||||
|> Ash.Query.sort(:name)
|
|> Ash.Query.sort(:name)
|
||||||
|> Ash.Query.limit(2)
|
|> Ash.Query.limit(2)
|
||||||
|
|
||||||
assert [^joe, ^matthew] = EtsApiTest.read!(query)
|
assert [^joe, ^matthew] = clear_meta(EtsApiTest.read!(query))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "offset" do
|
test "offset" do
|
||||||
|
@ -146,7 +148,7 @@ defmodule Ash.DataLayer.EtsTest do
|
||||||
|> Ash.Query.sort(:name)
|
|> Ash.Query.sort(:name)
|
||||||
|> Ash.Query.offset(1)
|
|> Ash.Query.offset(1)
|
||||||
|
|
||||||
assert [^matthew, ^mike, ^zachary] = EtsApiTest.read!(query)
|
assert [^matthew, ^mike, ^zachary] = clear_meta(EtsApiTest.read!(query))
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "filter" do
|
describe "filter" do
|
||||||
|
@ -159,55 +161,61 @@ defmodule Ash.DataLayer.EtsTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "values", %{zachary: zachary, matthew: matthew, joe: joe} do
|
test "values", %{zachary: zachary, matthew: matthew, joe: joe} do
|
||||||
assert [^zachary] = filter_users(name: "Zachary")
|
assert [^zachary] = clear_meta(filter_users(name: "Zachary"))
|
||||||
assert [^joe] = filter_users(name: "Joe")
|
assert [^joe] = clear_meta(filter_users(name: "Joe"))
|
||||||
assert [^matthew] = filter_users(age: 9)
|
assert [^matthew] = clear_meta(filter_users(age: 9))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "or, in, eq", %{mike: mike, zachary: zachary, joe: joe} do
|
test "or, in, eq", %{mike: mike, zachary: zachary, joe: joe} do
|
||||||
assert [^joe, ^mike, ^zachary] =
|
assert [^joe, ^mike, ^zachary] =
|
||||||
filter_users(
|
clear_meta(
|
||||||
or: [
|
filter_users(
|
||||||
[name: [in: ["Zachary", "Mike"]]],
|
or: [
|
||||||
[age: [eq: 11]]
|
[name: [in: ["Zachary", "Mike"]]],
|
||||||
]
|
[age: [eq: 11]]
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "and, in, eq", %{mike: mike} do
|
test "and, in, eq", %{mike: mike} do
|
||||||
assert [^mike] =
|
assert [^mike] =
|
||||||
filter_users(
|
clear_meta(
|
||||||
and: [
|
filter_users(
|
||||||
[name: [in: ["Zachary", "Mike"]]],
|
and: [
|
||||||
[age: [eq: 37]]
|
[name: [in: ["Zachary", "Mike"]]],
|
||||||
]
|
[age: [eq: 37]]
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "and, in, not", %{zachary: zachary} do
|
test "and, in, not", %{zachary: zachary} do
|
||||||
assert [^zachary] =
|
assert [^zachary] =
|
||||||
filter_users(
|
clear_meta(
|
||||||
and: [
|
filter_users(
|
||||||
[name: [in: ["Zachary", "Mike"]]],
|
and: [
|
||||||
[not: [age: 37]]
|
[name: [in: ["Zachary", "Mike"]]],
|
||||||
]
|
[not: [age: 37]]
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "gt", %{mike: mike, joe: joe} do
|
test "gt", %{mike: mike, joe: joe} do
|
||||||
assert [^joe, ^mike] = filter_users(age: [gt: 10])
|
assert [^joe, ^mike] = clear_meta(filter_users(age: [gt: 10]))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "lt", %{zachary: zachary, matthew: matthew} do
|
test "lt", %{zachary: zachary, matthew: matthew} do
|
||||||
assert [^matthew, ^zachary] = filter_users(age: [lt: 10])
|
assert [^matthew, ^zachary] = clear_meta(filter_users(age: [lt: 10]))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "boolean", %{zachary: zachary, matthew: matthew} do
|
test "boolean", %{zachary: zachary, matthew: matthew} do
|
||||||
assert [^matthew, ^zachary] = filter_users(and: [true, age: [lt: 10]])
|
assert [^matthew, ^zachary] = clear_meta(filter_users(and: [true, age: [lt: 10]]))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "is_nil", %{zachary: zachary, matthew: matthew, joe: joe} do
|
test "is_nil", %{zachary: zachary, matthew: matthew, joe: joe} do
|
||||||
assert [^joe, ^matthew, ^zachary] = filter_users(title: [is_nil: true])
|
assert [^joe, ^matthew, ^zachary] = clear_meta(filter_users(title: [is_nil: true]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -140,6 +140,26 @@ defmodule Ash.Test.CalculationTest do
|
||||||
assert full_names == ["brian cranston", "zach daniel"]
|
assert full_names == ["brian cranston", "zach daniel"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "expression based calculations can be sorted on" do
|
||||||
|
full_names =
|
||||||
|
User
|
||||||
|
|> Ash.Query.load(:expr_full_name)
|
||||||
|
|> Ash.Query.sort(:expr_full_name)
|
||||||
|
|> Api.read!()
|
||||||
|
|> Enum.map(& &1.expr_full_name)
|
||||||
|
|
||||||
|
assert full_names == ["brian cranston", "zach daniel"]
|
||||||
|
|
||||||
|
full_names =
|
||||||
|
User
|
||||||
|
|> Ash.Query.load(:expr_full_name)
|
||||||
|
|> Ash.Query.sort(expr_full_name: :desc)
|
||||||
|
|> Api.read!()
|
||||||
|
|> Enum.map(& &1.expr_full_name)
|
||||||
|
|
||||||
|
assert full_names == ["zach daniel", "brian cranston"]
|
||||||
|
end
|
||||||
|
|
||||||
test "the `if` calculation resolves the first expr when true, and the second when false" do
|
test "the `if` calculation resolves the first expr when true, and the second when false" do
|
||||||
User
|
User
|
||||||
|> Ash.Changeset.new(%{first_name: "bob"})
|
|> Ash.Changeset.new(%{first_name: "bob"})
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Ash.Test.Filter.FilterTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
import Ash.Changeset
|
import Ash.Changeset
|
||||||
|
import Ash.Test.Helpers
|
||||||
|
|
||||||
alias Ash.Filter
|
alias Ash.Filter
|
||||||
|
|
||||||
|
@ -256,6 +257,7 @@ defmodule Ash.Test.Filter.FilterTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == ^post1.title)
|
|> Ash.Query.filter(title == ^post1.title)
|
||||||
|> Api.read!()
|
|> Api.read!()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "multiple filter field matches", %{post1: post1} do
|
test "multiple filter field matches", %{post1: post1} do
|
||||||
|
@ -263,6 +265,7 @@ defmodule Ash.Test.Filter.FilterTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == ^post1.title and contents == ^post1.contents)
|
|> Ash.Query.filter(title == ^post1.title and contents == ^post1.contents)
|
||||||
|> Api.read!()
|
|> Api.read!()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "no field matches" do
|
test "no field matches" do
|
||||||
|
@ -290,12 +293,14 @@ defmodule Ash.Test.Filter.FilterTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(points < 2)
|
|> Ash.Query.filter(points < 2)
|
||||||
|> Api.read!()
|
|> Api.read!()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
assert [^post1, ^post2] =
|
assert [^post1, ^post2] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(points < 3)
|
|> Ash.Query.filter(points < 3)
|
||||||
|> Ash.Query.sort(points: :asc)
|
|> Ash.Query.sort(points: :asc)
|
||||||
|> Api.read!()
|
|> Api.read!()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "greater than works", %{
|
test "greater than works", %{
|
||||||
|
@ -306,12 +311,14 @@ defmodule Ash.Test.Filter.FilterTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(points > 1)
|
|> Ash.Query.filter(points > 1)
|
||||||
|> Api.read!()
|
|> Api.read!()
|
||||||
|
|> clear_meta()
|
||||||
|
|
||||||
assert [^post1, ^post2] =
|
assert [^post1, ^post2] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(points > 0)
|
|> Ash.Query.filter(points > 0)
|
||||||
|> Ash.Query.sort(points: :asc)
|
|> Ash.Query.sort(points: :asc)
|
||||||
|> Api.read!()
|
|> Api.read!()
|
||||||
|
|> clear_meta()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
27
test/support/helpers.ex
Normal file
27
test/support/helpers.ex
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Ash.Test.Helpers do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
def clear_meta({:ok, record}) do
|
||||||
|
{:ok, clear_meta(record)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_meta({:error, error}), do: {:error, error}
|
||||||
|
|
||||||
|
def clear_meta(value) when is_list(value) do
|
||||||
|
Enum.map(value, &clear_meta/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_meta(%Ash.Page.Offset{results: results} = page) do
|
||||||
|
%{page | results: Enum.map(results, &clear_meta/1)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_meta(%Ash.Page.Keyset{results: results} = page) do
|
||||||
|
%{page | results: Enum.map(results, &clear_meta/1)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_meta(%{__metadata__: _} = record) do
|
||||||
|
Map.put(record, :__metadata__, %{})
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_meta(other), do: other
|
||||||
|
end
|
Loading…
Reference in a new issue