mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
parent
949bae3922
commit
d600c55509
12 changed files with 117 additions and 122 deletions
|
@ -32,8 +32,11 @@ locals_without_parens = [
|
|||
destroy: 2,
|
||||
event: 1,
|
||||
expensive?: 1,
|
||||
field: 1,
|
||||
field_type: 1,
|
||||
filter: 1,
|
||||
first: 3,
|
||||
first: 4,
|
||||
generated?: 1,
|
||||
global?: 1,
|
||||
has_many: 2,
|
||||
|
@ -67,6 +70,7 @@ locals_without_parens = [
|
|||
resource: 1,
|
||||
resource: 2,
|
||||
soft?: 1,
|
||||
sort: 1,
|
||||
source_field: 1,
|
||||
source_field_on_join_table: 1,
|
||||
strategy: 1,
|
||||
|
|
|
@ -27,7 +27,9 @@ defmodule Ash do
|
|||
@type resource :: module
|
||||
@type side_loads :: term
|
||||
@type page :: Ash.Page.Keyset.t() | Ash.Page.Offset.t()
|
||||
@type sort :: list(atom | {atom, :asc} | {atom, :desc})
|
||||
@type sort_order ::
|
||||
:asc | :desc | :asc_nils_first | :asc_nils_last | :desc_nils_first | :desc_nils_last
|
||||
@type sort :: list(atom | {atom, sort_order})
|
||||
@type validation :: Ash.Resource.Validation.t()
|
||||
@type notification :: Ash.Notifier.Notification.t()
|
||||
|
||||
|
|
|
@ -593,7 +593,7 @@ defmodule Ash.Actions.Read do
|
|||
},
|
||||
query
|
||||
) do
|
||||
case Ash.Query.Aggregate.new(destination_resource, :count, :count, [], nil) do
|
||||
case Ash.Query.Aggregate.new(destination_resource, :count, :count, [], nil, nil) do
|
||||
{:ok, aggregate} ->
|
||||
Ash.DataLayer.run_aggregate_query_with_lateral_join(
|
||||
query,
|
||||
|
@ -611,7 +611,7 @@ defmodule Ash.Actions.Read do
|
|||
end
|
||||
|
||||
defp run_count_query(ash_query, query) do
|
||||
case Ash.Query.Aggregate.new(ash_query.resource, :count, :count, [], nil) do
|
||||
case Ash.Query.Aggregate.new(ash_query.resource, :count, :count, [], nil, nil) do
|
||||
{:ok, aggregate} ->
|
||||
Ash.DataLayer.run_aggregate_query(query, [aggregate], ash_query.resource)
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@ defmodule Ash.Actions.Sort do
|
|||
UnsortableAttribute
|
||||
}
|
||||
|
||||
@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, sort, aggregates) when is_list(sort) do
|
||||
sort
|
||||
|> Enum.reduce({[], []}, fn
|
||||
{field, order}, {sorts, errors} when order in [:asc, :desc] ->
|
||||
{field, order}, {sorts, errors} when order in @sort_orders ->
|
||||
attribute = Ash.Resource.attribute(resource, field)
|
||||
|
||||
cond do
|
||||
|
@ -57,18 +59,6 @@ defmodule Ash.Actions.Sort do
|
|||
end)
|
||||
end
|
||||
|
||||
def reverse(sort) do
|
||||
Enum.map(sort, fn {field, direction} ->
|
||||
case direction do
|
||||
:asc ->
|
||||
{field, :desc}
|
||||
|
||||
:desc ->
|
||||
{field, :asc}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp aggregate_sort(aggregates, field, order, resource, sorts, errors) do
|
||||
aggregate = Map.get(aggregates, field)
|
||||
|
||||
|
@ -115,6 +105,46 @@ defmodule Ash.Actions.Sort do
|
|||
defp to_sort_by_fun(:desc),
|
||||
do: &(elem(&1, 1) >= elem(&2, 1))
|
||||
|
||||
defp to_sort_by_fun(:asc_nils_last) do
|
||||
fn x, y ->
|
||||
if is_nil(elem(x, 1)) && !is_nil(elem(y, 1)) do
|
||||
false
|
||||
else
|
||||
elem(x, 1) <= elem(y, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp to_sort_by_fun(:asc_nils_first) do
|
||||
fn x, y ->
|
||||
if is_nil(elem(x, 1)) && !is_nil(elem(y, 1)) do
|
||||
true
|
||||
else
|
||||
elem(x, 1) <= elem(y, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp to_sort_by_fun(:desc_nulls_first) do
|
||||
fn x, y ->
|
||||
if is_nil(elem(x, 1)) && !is_nil(elem(y, 1)) do
|
||||
true
|
||||
else
|
||||
elem(x, 1) >= elem(y, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp to_sort_by_fun(:desc_nulls_last) do
|
||||
fn x, y ->
|
||||
if is_nil(elem(x, 1)) && !is_nil(elem(y, 1)) do
|
||||
false
|
||||
else
|
||||
elem(x, 1) >= elem(y, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp to_sort_by_fun(module) when is_atom(module),
|
||||
do: &(module.compare(elem(&1, 1), elem(&2, 1)) != :gt)
|
||||
|
||||
|
|
|
@ -547,46 +547,6 @@ defmodule Ash.Api do
|
|||
read(api, query, Keyword.put(opts, :page, page_opts))
|
||||
end
|
||||
|
||||
def page(api, %Ash.Page.Keyset{rerun: {query, opts}}, :last) do
|
||||
query_reverse_sorted =
|
||||
case query.sort do
|
||||
nil ->
|
||||
sort =
|
||||
query.resource
|
||||
|> Ash.Resource.primary_key()
|
||||
|> Enum.map(&{&1, :desc})
|
||||
|
||||
Ash.Query.sort(query, sort)
|
||||
|
||||
sort ->
|
||||
new_sorted =
|
||||
query
|
||||
|> Ash.Query.unset(:sort)
|
||||
|> Ash.Query.sort(Ash.Actions.Sort.reverse(sort))
|
||||
|
||||
if Ash.Actions.Sort.sorting_on_identity?(new_sorted) do
|
||||
new_sorted
|
||||
else
|
||||
sort =
|
||||
query.resource
|
||||
|> Ash.Resource.primary_key()
|
||||
|> Enum.map(&{&1, :desc})
|
||||
|
||||
Ash.Query.sort(new_sorted, sort)
|
||||
end
|
||||
end
|
||||
|
||||
new_page_params = Keyword.drop(opts[:page] || [], [:before, :after])
|
||||
|
||||
case read(api, query_reverse_sorted, Keyword.put(opts, :page, new_page_params)) do
|
||||
{:ok, page} ->
|
||||
{:ok, Map.update!(page, :results, &Enum.reverse/1)}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def page(
|
||||
api,
|
||||
%Ash.Page.Offset{count: count, limit: limit, offset: offset, rerun: {query, opts}},
|
||||
|
@ -622,17 +582,7 @@ defmodule Ash.Api do
|
|||
end
|
||||
|
||||
if request == :last && !count do
|
||||
case read(
|
||||
api,
|
||||
Ash.Query.reverse(query),
|
||||
Keyword.put(opts, :page, page_opts)
|
||||
) do
|
||||
{:ok, page} ->
|
||||
{:ok, Map.update!(page, :results, &Enum.reverse/1)}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
{:error, "Cannot fetch last page without counting"}
|
||||
else
|
||||
read(api, query, Keyword.put(opts, :page, page_opts))
|
||||
end
|
||||
|
|
|
@ -85,9 +85,17 @@ defmodule Ash.Page.Keyset do
|
|||
end
|
||||
|
||||
defp operator(:after, :asc), do: :gt
|
||||
defp operator(:after, :asc_nils_first), do: :gt
|
||||
defp operator(:after, :asc_nils_last), do: :gt
|
||||
defp operator(:after, :desc), do: :lt
|
||||
defp operator(:after, :desc_nulls_first), do: :lt
|
||||
defp operator(:after, :desc_nulls_last), do: :lt
|
||||
defp operator(:before, :asc), do: :lt
|
||||
defp operator(:before, :asc_nils_first), do: :lt
|
||||
defp operator(:before, :asc_nils_last), do: :lt
|
||||
defp operator(:before, :desc), do: :gt
|
||||
defp operator(:before, :desc_nulls_first), do: :gt
|
||||
defp operator(:before, :desc_nulls_last), do: :gt
|
||||
|
||||
defp zip_fields(pkey, values, acc \\ [])
|
||||
defp zip_fields([], [], acc), do: {:ok, Enum.reverse(acc)}
|
||||
|
|
|
@ -6,13 +6,14 @@ defmodule Ash.Query.Aggregate do
|
|||
:default_value,
|
||||
:resource,
|
||||
:query,
|
||||
:field,
|
||||
:kind,
|
||||
:type,
|
||||
:authorization_filter,
|
||||
:load
|
||||
]
|
||||
|
||||
@kinds [:count]
|
||||
@kinds [:count, :first]
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@type kind :: unquote(Enum.reduce(@kinds, &{:|, [], [&1, &2]}))
|
||||
|
@ -25,8 +26,14 @@ defmodule Ash.Query.Aggregate do
|
|||
alias Ash.Actions.SideLoad
|
||||
alias Ash.Engine.Request
|
||||
|
||||
def new(resource, name, kind, relationship, query) do
|
||||
with {:ok, type} <- kind_to_type(kind),
|
||||
def new(resource, name, kind, relationship, query, field) do
|
||||
field_type =
|
||||
if field do
|
||||
related = Ash.Resource.related(resource, relationship)
|
||||
Ash.Resource.attribute(related, field).type
|
||||
end
|
||||
|
||||
with {:ok, type} <- kind_to_type(kind, field_type),
|
||||
{:ok, query} <- validate_query(query) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
|
@ -34,6 +41,7 @@ defmodule Ash.Query.Aggregate do
|
|||
resource: resource,
|
||||
default_value: default_value(kind),
|
||||
relationship_path: List.wrap(relationship),
|
||||
field: field,
|
||||
kind: kind,
|
||||
type: type,
|
||||
query: query
|
||||
|
@ -42,6 +50,7 @@ defmodule Ash.Query.Aggregate do
|
|||
end
|
||||
|
||||
defp default_value(:count), do: 0
|
||||
defp default_value(:first), do: nil
|
||||
|
||||
defp validate_query(nil), do: {:ok, nil}
|
||||
|
||||
|
@ -53,9 +62,6 @@ defmodule Ash.Query.Aggregate do
|
|||
query.aggregates != %{} ->
|
||||
{:error, "Cannot aggregate in an aggregate"}
|
||||
|
||||
query.sort != [] ->
|
||||
{:error, "Cannot sort an aggregate (for now)"}
|
||||
|
||||
not is_nil(query.limit) ->
|
||||
{:error, "Cannot limit an aggregate (for now)"}
|
||||
|
||||
|
@ -68,8 +74,10 @@ defmodule Ash.Query.Aggregate do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def kind_to_type(:count), do: {:ok, Ash.Type.Integer}
|
||||
def kind_to_type(kind), do: {:error, "Invalid aggregate kind: #{kind}"}
|
||||
def kind_to_type(:count, _field_type), do: {:ok, Ash.Type.Integer}
|
||||
def kind_to_type(:first, nil), do: {:error, "Must provide field type for :first"}
|
||||
def kind_to_type(:first, field_type), do: {:ok, field_type}
|
||||
def kind_to_type(kind, _field_type), do: {:error, "Invalid aggregate kind: #{kind}"}
|
||||
|
||||
def requests(initial_query, can_be_in_query?, authorizing?) do
|
||||
initial_query.aggregates
|
||||
|
|
|
@ -302,14 +302,15 @@ defmodule Ash.Query do
|
|||
related = Ash.Resource.related(query.resource, aggregate.relationship_path)
|
||||
|
||||
with %{valid?: true} = aggregate_query <-
|
||||
build(related, filter: aggregate.filter),
|
||||
build(related, filter: aggregate.filter, sort: aggregate.sort),
|
||||
{:ok, query_aggregate} <-
|
||||
Aggregate.new(
|
||||
query.resource,
|
||||
aggregate.name,
|
||||
aggregate.kind,
|
||||
aggregate.relationship_path,
|
||||
aggregate_query
|
||||
aggregate_query,
|
||||
aggregate.field
|
||||
) do
|
||||
query_aggregate = %{query_aggregate | load: field}
|
||||
new_aggregates = Map.put(query.aggregates, aggregate.name, query_aggregate)
|
||||
|
@ -555,7 +556,7 @@ defmodule Ash.Query do
|
|||
atom | list(atom),
|
||||
Ash.query() | nil
|
||||
) :: t()
|
||||
def aggregate(query, name, type, relationship, agg_query \\ nil) do
|
||||
def aggregate(query, name, type, relationship, field \\ nil, agg_query \\ nil) do
|
||||
query = to_query(query)
|
||||
relationship = List.wrap(relationship)
|
||||
|
||||
|
@ -572,7 +573,7 @@ defmodule Ash.Query do
|
|||
build(Ash.Resource.related(query.resource, relationship), options)
|
||||
end
|
||||
|
||||
case Aggregate.new(query.resource, name, type, relationship, agg_query) do
|
||||
case Aggregate.new(query.resource, name, type, relationship, agg_query, field) do
|
||||
{:ok, aggregate} ->
|
||||
new_aggregates = Map.put(query.aggregates, aggregate.name, aggregate)
|
||||
|
||||
|
@ -814,22 +815,6 @@ defmodule Ash.Query do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reverse the sort order of a query.
|
||||
|
||||
If the query has no sort, an error is added indicating that.
|
||||
"""
|
||||
@spec reverse(t()) :: t()
|
||||
def reverse(%{sort: nil} = query) do
|
||||
add_error(query, :sort, "Unreversable sort")
|
||||
end
|
||||
|
||||
def reverse(query) do
|
||||
query
|
||||
|> Ash.Query.unset(:sort)
|
||||
|> Ash.Query.sort(Ash.Actions.Sort.reverse(query.sort))
|
||||
end
|
||||
|
||||
@spec unset(Ash.resource() | t(), atom | [atom]) :: t()
|
||||
def unset(query, keys) when is_list(keys) do
|
||||
query = to_query(query)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Ash.Resource.Aggregate do
|
||||
@moduledoc "Represents a named aggregate on the resource that can be loaded"
|
||||
defstruct [:name, :relationship_path, :filter, :kind, :description, :private?]
|
||||
defstruct [:name, :relationship_path, :filter, :kind, :description, :private?, :field, :sort]
|
||||
|
||||
@schema [
|
||||
name: [
|
||||
|
@ -18,11 +18,21 @@ defmodule Ash.Resource.Aggregate do
|
|||
doc: "The kind of the aggregate",
|
||||
required: true
|
||||
],
|
||||
field: [
|
||||
type: :atom,
|
||||
doc:
|
||||
"The field to aggregate. Defaults to the first field in the primary key of the resource",
|
||||
required: false
|
||||
],
|
||||
filter: [
|
||||
type: :keyword_list,
|
||||
doc: "A filter to apply to the aggregate",
|
||||
default: []
|
||||
],
|
||||
sort: [
|
||||
type: :any,
|
||||
doc: "A sort to be applied to the aggregate"
|
||||
],
|
||||
description: [
|
||||
type: :string,
|
||||
doc: "An optional description for the aggregate"
|
||||
|
@ -39,7 +49,8 @@ defmodule Ash.Resource.Aggregate do
|
|||
name: atom(),
|
||||
relationship_path: {:ok, list(atom())} | {:error, String.t()},
|
||||
filter: Keyword.t(),
|
||||
kind: :count,
|
||||
field: atom,
|
||||
kind: Ash.Query.Aggregate.kind(),
|
||||
description: String.t() | nil,
|
||||
private?: boolean
|
||||
}
|
||||
|
|
|
@ -563,7 +563,7 @@ defmodule Ash.Resource.Dsl do
|
|||
@count %Ash.Dsl.Entity{
|
||||
name: :count,
|
||||
describe: """
|
||||
Declares a named aggregate on the resource
|
||||
Declares a named count aggregate on the resource
|
||||
""",
|
||||
examples: [
|
||||
"""
|
||||
|
@ -578,6 +578,24 @@ defmodule Ash.Resource.Dsl do
|
|||
auto_set_fields: [kind: :count]
|
||||
}
|
||||
|
||||
@first %Ash.Dsl.Entity{
|
||||
name: :first,
|
||||
describe: """
|
||||
Declares a named aggregate on the resource
|
||||
""",
|
||||
examples: [
|
||||
"""
|
||||
count :assigned_ticket_count, :reported_tickets do
|
||||
filter [active: true]
|
||||
end
|
||||
"""
|
||||
],
|
||||
target: Ash.Resource.Aggregate,
|
||||
args: [:name, :relationship_path, :field],
|
||||
schema: Ash.Resource.Aggregate.schema(),
|
||||
auto_set_fields: [kind: :first]
|
||||
}
|
||||
|
||||
@aggregates %Ash.Dsl.Section{
|
||||
name: :aggregates,
|
||||
describe: """
|
||||
|
@ -596,7 +614,8 @@ defmodule Ash.Resource.Dsl do
|
|||
"""
|
||||
],
|
||||
entities: [
|
||||
@count
|
||||
@count,
|
||||
@first
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ defmodule Ash.Schema do
|
|||
field(:__metadata__, :map, virtual: true, default: %{})
|
||||
|
||||
for aggregate <- Ash.Resource.aggregates(__MODULE__) do
|
||||
{:ok, type} = Aggregate.kind_to_type(aggregate.kind)
|
||||
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, :string)
|
||||
|
||||
field(aggregate.name, Ash.Type.ecto_type(type),
|
||||
virtual: true,
|
||||
|
|
|
@ -195,17 +195,6 @@ defmodule Ash.Actions.PaginationTest do
|
|||
assert %{results: [%{name: "4"}]} = Api.page!(page, :first)
|
||||
end
|
||||
|
||||
test "the last page can be fetched if the count was not requested" do
|
||||
assert %{results: [%{name: "3"}]} =
|
||||
page =
|
||||
User
|
||||
|> Ash.Query.sort(name: :desc)
|
||||
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
||||
|> Api.read!(page: [offset: 1, limit: 1])
|
||||
|
||||
assert %{results: [%{name: "0"}]} = Api.page!(page, :last)
|
||||
end
|
||||
|
||||
test "the last page can be fetched if the count was requested" do
|
||||
assert %{results: [%{name: "3"}]} =
|
||||
page =
|
||||
|
@ -370,17 +359,6 @@ defmodule Ash.Actions.PaginationTest do
|
|||
assert %{results: [%{name: "3"}]} = page = Api.page!(page, :next)
|
||||
assert %{results: [%{name: "4"}]} = Api.page!(page, :first)
|
||||
end
|
||||
|
||||
test "the last page can be fetched" do
|
||||
assert %{results: [%{name: "3"}]} =
|
||||
page =
|
||||
User
|
||||
|> Ash.Query.sort(name: :desc)
|
||||
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
||||
|> Api.read!(page: [offset: 1, limit: 1])
|
||||
|
||||
assert %{results: [%{name: "0"}]} = Api.page!(page, :last)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when both are supported" do
|
||||
|
|
Loading…
Reference in a new issue