Skip to content

Commit

Permalink
fix: better placed validations of aggregate support for data layers
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jan 26, 2025
1 parent 24045c3 commit 0ce7feb
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 150 deletions.
34 changes: 23 additions & 11 deletions lib/ash/actions/aggregate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,29 @@ defmodule Ash.Actions.Aggregate do
end)
|> case do
{:ok, aggregates} ->
{:ok,
Enum.map(aggregates, fn aggregate ->
Ash.Actions.Read.add_calc_context(
aggregate,
opts[:actor],
opts[:authorize?],
opts[:tenant],
opts[:tracer],
query.domain
)
end)}
Enum.reduce_while(aggregates, {:ok, []}, fn aggregate, {:ok, aggregates} ->
if Ash.DataLayer.data_layer_can?(aggregate.resource, {:query_aggregate, aggregate.kind}) do
aggregate =
Ash.Actions.Read.add_calc_context(
aggregate,
opts[:actor],
opts[:authorize?],
opts[:tenant],
opts[:tracer],
query.domain
)

{:cont, {:ok, [aggregate | aggregates]}}
else
{:halt,
{:error,
Ash.Error.Query.AggregatesNotSupported.exception(
resource: aggregate.resource,
feature: "using",
type: :query_aggregate
)}}
end
end)

other ->
other
Expand Down
15 changes: 12 additions & 3 deletions lib/ash/error/query/aggregates_not_supported.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ defmodule Ash.Error.Query.AggregatesNotSupported do
@moduledoc "Used when the data_layer does not support aggregates, or filtering/sorting them"
use Ash.Error.Exception

use Splode.Error, fields: [:resource, :feature], class: :invalid
use Splode.Error, fields: [:resource, :feature, type: :aggregate], class: :invalid

def message(%{resource: resource, feature: feature}) do
"Data layer for #{inspect(resource)} does not support #{feature} aggregates"
def message(%{resource: resource, feature: feature, type: type}) do
type =
case type do
:aggregate ->
"aggregates"

:query_aggregate ->
"query aggregates"
end

"Data layer for #{inspect(resource)} does not support #{feature} #{type}"
end
end
251 changes: 140 additions & 111 deletions lib/ash/filter/filter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2783,56 +2783,65 @@ defmodule Ash.Filter do
end

aggregate = aggregate(context, field) ->
related = Ash.Resource.Info.related(context.resource, aggregate.relationship_path)
if Ash.DataLayer.data_layer_can?(context.resource, {:aggregate, aggregate.kind}) do
related = Ash.Resource.Info.related(context.resource, aggregate.relationship_path)

read_action =
aggregate.read_action || Ash.Resource.Info.primary_action!(related, :read).name

with %{valid?: true} = aggregate_query <- Ash.Query.for_read(related, read_action),
%{valid?: true} = aggregate_query <-
Ash.Query.Aggregate.build_query(
aggregate_query,
context.resource,
filter: aggregate.filter,
sort: aggregate.sort
),
{:ok, query_aggregate} <-
Aggregate.new(
context.resource,
aggregate.name,
aggregate.kind,
agg_name: aggregate.name,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
sortable?: aggregate.sortable?,
sensitive?: aggregate.sensitive?,
type: aggregate.type,
include_nil?: aggregate.include_nil?,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action: read_action,
authorize?: aggregate.authorize?,
join_filters:
Map.new(aggregate.join_filters, &{&1.relationship_path, &1.filter})
) do
query_aggregate = %{query_aggregate | load: aggregate.name}

case parse_predicates(nested_statement, query_aggregate, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}

read_action =
aggregate.read_action || Ash.Resource.Info.primary_action!(related, :read).name

with %{valid?: true} = aggregate_query <- Ash.Query.for_read(related, read_action),
%{valid?: true} = aggregate_query <-
Ash.Query.Aggregate.build_query(
aggregate_query,
context.resource,
filter: aggregate.filter,
sort: aggregate.sort
),
{:ok, query_aggregate} <-
Aggregate.new(
context.resource,
aggregate.name,
aggregate.kind,
agg_name: aggregate.name,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
sortable?: aggregate.sortable?,
sensitive?: aggregate.sensitive?,
type: aggregate.type,
include_nil?: aggregate.include_nil?,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action: read_action,
authorize?: aggregate.authorize?,
join_filters: Map.new(aggregate.join_filters, &{&1.relationship_path, &1.filter})
) do
query_aggregate = %{query_aggregate | load: aggregate.name}

case parse_predicates(nested_statement, query_aggregate, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
else
%{valid?: false, errors: errors} ->
{:error, errors}

{:error, error} ->
{:error, error}
end
else
%{valid?: false, errors: errors} ->
{:error, errors}

{:error, error} ->
{:error, error}
{:error,
Ash.Error.Query.AggregatesNotSupported.exception(
resource: context.resource,
feature: "using"
)}
end

resource_calculation = calculation(context, field) ->
Expand Down Expand Up @@ -3155,31 +3164,36 @@ defmodule Ash.Filter do

opts = Enum.at(args, 1) || []

if Keyword.keyword?(opts) do
kind =
if name == :custom_aggregate do
:custom
else
name
end
kind =
if name == :custom_aggregate do
:custom
else
name
end

opts = Keyword.put(opts, :path, path)
if Ash.DataLayer.data_layer_can?(context.resource, {:aggregate, kind}) do
if Keyword.keyword?(opts) do
opts = Keyword.put(opts, :path, path)

with {:ok, agg} <-
Aggregate.new(
resource,
agg_name(kind, opts),
kind,
opts
) do
{:ok,
%Ref{
relationship_path: call.relationship_path,
attribute: agg
}}
with {:ok, agg} <-
Aggregate.new(
resource,
agg_name(kind, opts),
kind,
opts
) do
{:ok,
%Ref{
relationship_path: call.relationship_path,
attribute: agg
}}
end
else
{:error, "Aggregate options must be keyword list. In: #{inspect(call)}"}
end
else
{:error, "Aggregate options must be keyword list. In: #{inspect(call)}"}
{:error,
Ash.Error.Query.AggregatesNotSupported.exception(resource: resource, feature: "using")}
end
end

Expand Down Expand Up @@ -3467,6 +3481,18 @@ defmodule Ash.Filter do
end
end

def do_hydrate_refs(
%Ref{attribute: %Ash.Query.Aggregate{} = agg} = ref,
_context
) do
if Ash.DataLayer.data_layer_can?(agg.resource, {:aggregate, agg.kind}) do
{:ok, ref}
else
{:error,
Ash.Error.Query.AggregatesNotSupported.exception(resource: agg.resource, feature: "using")}
end
end

def do_hydrate_refs(
%Ref{attribute: attribute} = ref,
context
Expand Down Expand Up @@ -3498,49 +3524,57 @@ defmodule Ash.Filter do
end

aggregate = aggregate(context, attribute) ->
agg_related = Ash.Resource.Info.related(related, aggregate.relationship_path)

with %{valid?: true} = aggregate_query <-
Ash.Query.new(agg_related),
%{valid?: true} = aggregate_query <-
Ash.Query.Aggregate.build_query(
aggregate_query,
context.resource,
filter: aggregate.filter,
sort: aggregate.sort
),
{:ok, query_aggregate} <-
Aggregate.new(
related,
aggregate.name,
aggregate.kind,
agg_name: aggregate.name,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
type: aggregate.type,
sortable?: aggregate.sortable?,
include_nil?: aggregate.include_nil?,
sensitive?: aggregate.sensitive?,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action: aggregate.read_action,
authorize?: aggregate.authorize?,
join_filters:
Map.new(aggregate.join_filters, &{&1.relationship_path, &1.filter})
) do
query_aggregate = %{query_aggregate | load: aggregate.name}
if Ash.DataLayer.data_layer_can?(context.resource, {:aggregate, aggregate.kind}) do
agg_related = Ash.Resource.Info.related(related, aggregate.relationship_path)

with %{valid?: true} = aggregate_query <-
Ash.Query.new(agg_related),
%{valid?: true} = aggregate_query <-
Ash.Query.Aggregate.build_query(
aggregate_query,
context.resource,
filter: aggregate.filter,
sort: aggregate.sort
),
{:ok, query_aggregate} <-
Aggregate.new(
related,
aggregate.name,
aggregate.kind,
agg_name: aggregate.name,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
type: aggregate.type,
sortable?: aggregate.sortable?,
include_nil?: aggregate.include_nil?,
sensitive?: aggregate.sensitive?,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action: aggregate.read_action,
authorize?: aggregate.authorize?,
join_filters:
Map.new(aggregate.join_filters, &{&1.relationship_path, &1.filter})
) do
query_aggregate = %{query_aggregate | load: aggregate.name}

{:ok, %{ref | attribute: query_aggregate, resource: related}}
else
%{valid?: false, errors: errors} ->
{:error, errors}

{:ok, %{ref | attribute: query_aggregate, resource: related}}
{:error, error} ->
{:error, error}
end
else
%{valid?: false, errors: errors} ->
{:error, errors}

{:error, error} ->
{:error, error}
{:error,
Ash.Error.Query.AggregatesNotSupported.exception(
resource: context.resource,
feature: "using"
)}
end

relationship = relationship(context, attribute) ->
Expand All @@ -3566,11 +3600,6 @@ defmodule Ash.Filter do
end
end

def do_hydrate_refs(%Ref{relationship_path: relationship_path, resource: nil} = ref, context) do
ref = %{ref | input?: ref.input? || context[:input?] || false}
{:ok, %{ref | resource: Ash.Resource.Info.related(context.resource, relationship_path)}}
end

def do_hydrate_refs(%BooleanExpression{left: left, right: right} = expr, context) do
case do_hydrate_refs(left, context) do
{:ok, true} ->
Expand Down
Loading

0 comments on commit 0ce7feb

Please sign in to comment.