fix: properly handle limit/offset for aggregates

This commit is contained in:
Zach Daniel 2024-02-24 20:21:13 -05:00
parent adbac1b7da
commit eab194fac5
3 changed files with 41 additions and 19 deletions

View file

@ -5,8 +5,6 @@ defmodule Ash.Actions.Aggregate do
def run(api, query, aggregates, opts) do def run(api, query, aggregates, opts) do
query = %{query | api: api} query = %{query | api: api}
{query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts) {query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts)
action = query.action || Ash.Resource.Info.primary_action!(query.resource, :read)
opts = Keyword.put_new(opts, :read_action, action.name)
with %{valid?: true} = query <- Ash.Actions.Read.handle_attribute_multitenancy(query) do with %{valid?: true} = query <- Ash.Actions.Read.handle_attribute_multitenancy(query) do
aggregates aggregates
@ -15,19 +13,24 @@ defmodule Ash.Actions.Aggregate do
agg_authorize? = aggregate.authorize? && opts[:authorize?] agg_authorize? = aggregate.authorize? && opts[:authorize?]
read_action = read_action =
aggregate.read_action || query.action || aggregate.read_action || (query.action && query.action.name) ||
Ash.Resource.Info.primary_action!(query.resource, :read).name Ash.Resource.Info.primary_action!(query.resource, :read).name
{agg_authorize?, read_action} {agg_authorize?, read_action}
{_name, _kind} -> {_name, _kind} ->
{!!opts[:authorize?], opts[:read_action]} {!!opts[:authorize?],
opts[:read_action] || opts[:action] || (query.action && query.action.name) ||
Ash.Resource.Info.primary_action!(query.resource, :read).name}
{_name, _kind, agg_opts} -> {_name, _kind, agg_opts} ->
authorize? = authorize? =
Keyword.get(agg_opts, :authorize?, true) && opts[:authorize?] Keyword.get(agg_opts, :authorize?, true) && opts[:authorize?]
{authorize?, agg_opts[:read_action] || opts[:read_action] || action.name} {authorize?,
agg_opts[:read_action] || opts[:read_action] || agg_opts[:action] || opts[:action] ||
(query.action && query.action.name) ||
Ash.Resource.Info.primary_action!(query.resource, :read).name}
end) end)
|> Enum.reduce_while({:ok, %{}}, fn |> Enum.reduce_while({:ok, %{}}, fn
{{agg_authorize?, read_action}, aggregates}, {:ok, acc} -> {{agg_authorize?, read_action}, aggregates}, {:ok, acc} ->
@ -54,7 +57,7 @@ defmodule Ash.Actions.Aggregate do
aggregates: List.wrap(aggregates), aggregates: List.wrap(aggregates),
actor: opts[:actor], actor: opts[:actor],
tenant: opts[:tenant], tenant: opts[:tenant],
action: action.name, action: read_action,
authorize?: opts[:authorize?] authorize?: opts[:authorize?]
} }
@ -65,7 +68,11 @@ defmodule Ash.Actions.Aggregate do
with {:ok, query} <- authorize_query(query, opts, agg_authorize?), with {:ok, query} <- authorize_query(query, opts, agg_authorize?),
{:ok, aggregates} <- validate_aggregates(query, aggregates, opts), {:ok, aggregates} <- validate_aggregates(query, aggregates, opts),
{:ok, data_layer_query} <- {:ok, data_layer_query} <-
Ash.Query.data_layer_query(%Ash.Query{resource: query.resource}), Ash.Query.data_layer_query(%Ash.Query{
resource: query.resource,
limit: query.limit,
offset: query.offset
}),
{:ok, result} <- {:ok, result} <-
Ash.DataLayer.run_aggregate_query( Ash.DataLayer.run_aggregate_query(
data_layer_query, data_layer_query,
@ -88,16 +95,10 @@ defmodule Ash.Actions.Aggregate do
|> Ash.Query.do_filter(right.filter) |> Ash.Query.do_filter(right.filter)
|> Ash.Query.sort(right.sort, prepend?: true) |> Ash.Query.sort(right.sort, prepend?: true)
|> Ash.Query.distinct_sort(right.distinct_sort, prepend?: true) |> Ash.Query.distinct_sort(right.distinct_sort, prepend?: true)
|> Ash.Query.limit(right.limit)
|> Ash.Query.set_tenant(right.tenant) |> Ash.Query.set_tenant(right.tenant)
|> merge_offset(right.offset)
|> Ash.Query.set_context(right.context) |> Ash.Query.set_context(right.context)
end end
defp merge_offset(query, offset) do
Ash.Query.offset(query, query.offset + (offset || 0))
end
defp authorize_query(query, opts, agg_authorize?) do defp authorize_query(query, opts, agg_authorize?) do
if agg_authorize? do if agg_authorize? do
case query.api.can(query, opts[:actor], case query.api.can(query, opts[:actor],
@ -155,6 +156,7 @@ defmodule Ash.Actions.Aggregate do
end end
defp set_opts(query, specified, others) do defp set_opts(query, specified, others) do
query = Ash.Query.unset(query, [:limit, :offset])
{agg_opts, _} = Ash.Query.Aggregate.split_aggregate_opts(others) {agg_opts, _} = Ash.Query.Aggregate.split_aggregate_opts(others)
agg_opts = Keyword.merge(agg_opts, specified) agg_opts = Keyword.merge(agg_opts, specified)

View file

@ -384,6 +384,14 @@ defmodule Ash.Query.Aggregate do
defp build_query(_resource, nil), do: {:ok, nil} defp build_query(_resource, nil), do: {:ok, nil}
defp build_query(resource, build_opts) when is_list(build_opts) do defp build_query(resource, build_opts) when is_list(build_opts) do
cond do
build_opts[:limit] ->
{:error, "Cannot set limit on aggregate query"}
build_opts[:offset] && build_opts[:offset] != 0 ->
{:error, "Cannot set offset on aggregate query"}
true ->
case Ash.Query.build(resource, build_opts) do case Ash.Query.build(resource, build_opts) do
%{valid?: true} = query -> %{valid?: true} = query ->
build_query(resource, query) build_query(resource, query)
@ -392,10 +400,20 @@ defmodule Ash.Query.Aggregate do
{:error, query.errors} {:error, query.errors}
end end
end end
end
defp build_query(_resource, %Ash.Query{} = query) do defp build_query(_resource, %Ash.Query{} = query) do
cond do
query.limit ->
{:error, "Cannot set limit on aggregate query"}
query.offset && query.offset != 0 ->
{:error, "Cannot set offset on aggregate query"}
true ->
{:ok, Ash.Query.unset(query, [:load, :limit, :offset])} {:ok, Ash.Query.unset(query, [:load, :limit, :offset])}
end end
end
@doc false @doc false
def kind_to_type({:custom, type}, _attribute_type, _attribute_constraints), do: {:ok, type, []} def kind_to_type({:custom, type}, _attribute_type, _attribute_constraints), do: {:ok, type, []}

View file

@ -195,6 +195,8 @@ defmodule Ash.Test.Actions.AggregateTest do
assert %{count: 0} = Api.aggregate!(Post, {:count, :count}, authorize?: true) assert %{count: 0} = Api.aggregate!(Post, {:count, :count}, authorize?: true)
assert 0 = Api.count!(Post, authorize?: true) assert 0 = Api.count!(Post, authorize?: true)
Application.put_env(:foo, :bar, true)
assert %{count: 1} = assert %{count: 1} =
Post Post
|> Ash.Query.for_read(:unpublic) |> Ash.Query.for_read(:unpublic)