Packages
ash
3.7.1
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/query/aggregate.ex
# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs.contributors>
#
# SPDX-License-Identifier: MIT
defmodule Ash.Query.Aggregate do
@moduledoc "Represents an aggregated association value"
defstruct [
:name,
:relationship_path,
:default_value,
:resource,
:query,
:field,
:kind,
:type,
:constraints,
:implementation,
:load,
:read_action,
:agg_name,
join_filters: %{},
context: %{},
authorize?: true,
include_nil?: false,
uniq?: false,
filterable?: true,
sortable?: true,
sensitive?: false,
related?: true
]
@type t :: %__MODULE__{}
@kinds [:count, :first, :sum, :list, :max, :min, :avg, :exists, :custom]
@type kind :: unquote(Enum.reduce(@kinds, &{:|, [], [&1, &2]}))
alias Ash.Error.Query.{NoReadAction, NoSuchRelationship}
require Ash.Query
@doc false
def kinds, do: @kinds
@doc false
def subpaths([]), do: []
def subpaths([first | rest]) do
[[first] | Enum.map(subpaths(rest), &[first | &1])]
end
@schema [
path: [
type: {:list, :atom},
doc:
"The relationship path to aggregate over. Only used when adding aggregates to a query.",
default: []
],
agg_name: [
type: :any,
hide: true,
doc: "A resource calculation this calculation maps to."
],
query: [
type: :any,
doc:
"A base query to use for the aggregate, or a keyword list to be passed to `Ash.Query.build/2`"
],
field: [
type: {:or, [:atom, {:struct, Ash.Query.Calculation}, {:struct, Ash.Query.Aggregate}]},
doc: "The field to use for the aggregate. Not necessary for all aggregate types."
],
expr: [
type: :any,
doc: "An expression to aggregate, cannot be used with `field`."
],
expr_type: [
type:
{:or,
[Ash.OptionsHelpers.ash_type(), {:tuple, [Ash.OptionsHelpers.ash_type(), :keyword_list]}]},
doc: "The type of the expression, required if `expr` is used."
],
arguments: [
type: :map,
doc: "Arguments to pass to the field, if field is a calculation."
],
default: [
type: :any,
doc: "A default value to use for the aggregate if it returns `nil`."
],
filterable?: [
type: :boolean,
doc: "Whether or not this aggregate may be used in filters.",
default: true
],
sortable?: [
type: :boolean,
doc: "Whether or not this aggregate may be used in sorts.",
default: true
],
type: [
type: :any,
doc: "A type to use for the aggregate."
],
constraints: [
type: :any,
doc: "Type constraints to use for the aggregate.",
default: []
],
implementation: [
type: :any,
doc: "The implementation for any custom aggregates."
],
read_action: [
type: :atom,
doc: "The read action to use for the aggregate, defaults to the primary read action."
],
uniq?: [
type: :boolean,
default: false,
doc:
"Whether or not to only consider unique values. Only relevant for `count` and `list` aggregates."
],
include_nil?: [
type: :boolean,
default: false,
doc:
"Whether or not to include `nil` values in the aggregate. Only relevant for `list` and `first` aggregates."
],
join_filters: [
type: {:map, {:wrap_list, :atom}, :any},
default: %{},
doc: """
A map of relationship paths (an atom or list of atoms), to an expression to apply when fetching the aggregate data. See the aggregates guide for more.
"""
],
sensitive?: [
type: :boolean,
doc: "Whether or not references to this aggregate will be considered sensitive",
default: false
],
tenant: [
type: :any,
doc: "The tenant to use for the aggregate, if applicable.",
default: nil
],
authorize?: [
type: :boolean,
default: true,
doc: """
Whether or not the aggregate query should authorize based on the target action.
See `d:Ash.Resource.Dsl.aggregates|count` for more information.
"""
],
resource: [
type: :atom,
doc: "For unrelated aggregates, the target resource to aggregate over"
],
related?: [
type: :boolean,
default: true,
doc:
"Whether this aggregate uses a relationship path (true) or a direct resource reference (false)"
]
]
@keys Keyword.keys(@schema)
@doc false
def opt_keys do
@keys
end
schema = @schema
defmodule Opts do
@moduledoc false
use Spark.Options.Validator, schema: schema
end
@doc """
Create a new aggregate, used with `Query.aggregate` or `Ash.aggregate`, raising on errors.
See `new/4` for more information.
### Options
#{Spark.Options.docs(@schema)}
"""
def new!(resource, name, kind, opts \\ []) do
case new(resource, name, kind, opts) do
{:ok, aggregate} ->
aggregate
{:error, error} ->
raise Ash.Error.to_error_class(error)
end
end
@doc """
Create a new aggregate, used with `Query.aggregate` or `Ash.aggregate`
### Options
#{Spark.Options.docs(@schema)}
"""
def new(resource, name, kind, opts \\ []) do
opts =
Enum.reject(opts, fn
{_key, nil} ->
true
_ ->
false
end)
with {:ok, %Opts{} = opts} <- Opts.validate(opts) do
agg_name = agg_name(opts)
{target_resource, related?} =
if opts.resource do
{opts.resource, false}
else
{Ash.Resource.Info.related(resource, opts.path), true}
end
query =
case opts.query || Ash.Query.new(target_resource) do
%Ash.Query{} = query -> query
build_opts -> build_query(target_resource, resource, build_opts)
end
Enum.reduce_while(opts.join_filters, {:ok, %{}}, fn {path, filter}, {:ok, acc} ->
case parse_join_filter(resource, path, filter) do
{:ok, filter} ->
{:cont, {:ok, Map.put(acc, path, filter)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, join_filters} ->
relationship_path = if related?, do: opts.path, else: []
field = opts.field
default = opts.default
filterable? = opts.filterable?
sortable? = opts.sortable?
sensitive? = opts.sensitive?
type = opts.type
constraints = opts.constraints
implementation = opts.implementation
uniq? = opts.uniq?
read_action =
if :read_action in opts.__set__ do
opts.read_action
else
if related? do
relationship = Ash.Resource.Info.relationship(resource, List.first(opts.path))
if relationship do
relationship.read_action
end
else
Ash.Resource.Info.primary_action!(opts.resource, :read).name
end
end
query =
if read_action && !query.action do
action = Ash.Resource.Info.action(target_resource, read_action)
%{query | action: action}
else
query
end
authorize? = opts.authorize?
include_nil? = opts.include_nil?
if kind == :custom && !type do
raise ArgumentError, "Must supply type when building a `custom` aggregate"
end
if kind == :custom && !implementation do
raise ArgumentError, "Must supply implementation when building a `custom` aggregate"
end
related = target_resource
if opts.field && opts.expr do
raise ArgumentError, "Cannot supply both `field` and `expr` for an aggregate"
end
{field, attribute_type, attribute_constraints} =
if opts.expr do
if is_nil(opts.expr_type) do
raise ArgumentError, "Must supply `expr_type` when using `expr`"
else
{expr_type, expr_constraints} =
case opts.expr_type do
{type, constraints} ->
{type, constraints}
type ->
{type, []}
end
name =
if is_binary(name) or is_atom(name) do
to_string(name)
else
inspect(name)
end
{:ok, calc} =
Ash.Query.Calculation.new(
"#{name}_expr",
Ash.Resource.Calculation.Expression,
[expr: opts.expr],
expr_type,
expr_constraints,
sensitive?: opts.sensitive?,
sortable?: opts.sortable?,
filterable?: opts.filterable?,
calc_name: nil
)
{calc, expr_type, expr_constraints}
end
else
if field do
case field do
%{type: type, constraints: constraints} ->
{field, type, constraints}
field when is_atom(field) ->
case Ash.Resource.Info.field(related, field) do
%{type: type, constraints: constraints} ->
{field, type, constraints}
%Ash.Resource.Aggregate{} = aggregate ->
{:ok, type, constraints} = aggregate_type(related, aggregate)
{field, type, constraints}
_ ->
raise "No such field for #{inspect(related)}: #{inspect(field)}"
end
end
else
{field, nil, nil}
end
end
field =
case field do
%Ash.Query.Calculation{} = field ->
field
%Ash.Query.Aggregate{} = field ->
field
field when is_atom(field) ->
case Ash.Resource.Info.field(related, field) do
%Ash.Resource.Calculation{} = calc ->
calc =
Ash.Query.Calculation.from_resource_calculation!(related, calc,
args: opts.arguments || %{}
)
%{calc | load: field}
_ ->
field
end
end
default =
if is_function(default) do
default.()
else
default
end
with :ok <- validate_uniq(uniq?, kind),
:ok <- if(related?, do: validate_path(resource, relationship_path), else: :ok),
{:ok, type, constraints} <-
get_type(kind, type, attribute_type, attribute_constraints, constraints),
%{valid?: true} = query <- build_query(related, resource, query) do
query =
if opts.tenant do
Ash.Query.set_tenant(query, opts.tenant)
else
query
end
{:ok,
%__MODULE__{
name: name,
agg_name: agg_name,
resource: if(related?, do: resource, else: target_resource),
constraints: constraints,
default_value: default || default_value(kind),
relationship_path: relationship_path,
implementation: implementation,
include_nil?: include_nil?,
field: field,
kind: kind,
type: type,
uniq?: uniq?,
query: query,
filterable?: filterable?,
sortable?: sortable?,
sensitive?: sensitive?,
authorize?: authorize?,
read_action: read_action,
join_filters:
Map.new(join_filters, fn {key, value} -> {List.wrap(key), value} end),
related?: related?
}}
else
%{valid?: false} = query ->
{:error, query.errors}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
end
end
defp agg_name(opts) do
if :agg_name in opts.__set__ do
opts.agg_name
end
end
defp parse_join_filter(resource, path, filter) do
[last_relationship | relationships] =
path_to_reversed_relationships(resource, path)
top_parent_resource = (List.last(relationships) || last_relationship).source
parent_resources =
relationships |> Enum.map(& &1.destination) |> Enum.concat([top_parent_resource])
Ash.Filter.parse(last_relationship.destination, filter, %{
parent_stack: parent_resources
})
end
defp path_to_reversed_relationships(resource, path, acc \\ [])
defp path_to_reversed_relationships(_resource, [], acc), do: acc
defp path_to_reversed_relationships(resource, [first | rest], acc) do
relationship = Ash.Resource.Info.relationship(resource, first)
if !relationship do
raise ArgumentError, "No such relationship: #{inspect(resource)}.#{first} in join_filter"
end
path_to_reversed_relationships(relationship.destination, rest, [
relationship | acc
])
end
defp validate_uniq(true, kind) when kind in [:count, :list], do: :ok
defp validate_uniq(true, kind),
do:
{:error,
"#{kind} aggregates do not support the `uniq?` option. Only count and list are supported currently."}
defp validate_uniq(_, _), do: :ok
defp get_type(:custom, type, _, _attribute_constraints, provided_constraints),
do: {:ok, type, provided_constraints || []}
defp get_type(_, type, _, _attribute_constraints, provided_constraints) when not is_nil(type),
do: {:ok, type, provided_constraints || []}
defp get_type(kind, _, attribute_type, attribute_constraints, provided_constraints) do
kind_to_type(kind, attribute_type, attribute_constraints || provided_constraints)
end
defp validate_path(_, []), do: :ok
defp validate_path(resource, [relationship | rest]) do
case Ash.Resource.Info.relationship(resource, relationship) do
nil ->
{:error, NoSuchRelationship.exception(resource: resource, relationship: relationship)}
%{type: :many_to_many, through: through, destination: destination} ->
cond do
!Ash.Resource.Info.primary_action(through, :read) ->
{:error, NoReadAction.exception(resource: through, when: "aggregating")}
!Ash.Resource.Info.primary_action(destination, :read) ->
{:error, NoReadAction.exception(resource: destination, when: "aggregating")}
!Ash.DataLayer.data_layer(through) == Ash.DataLayer.data_layer(resource) ->
{:error, "Cannot cross data layer boundaries when building an aggregate"}
true ->
validate_path(destination, rest)
end
relationship ->
cond do
!Ash.Resource.Info.primary_action(relationship.destination, :read) ->
NoReadAction.exception(resource: relationship.destination, when: "aggregating")
!Ash.DataLayer.data_layer(relationship.destination) ==
Ash.DataLayer.data_layer(resource) ->
{:error, "Cannot cross data layer boundaries when building an aggregate"}
true ->
validate_path(relationship.destination, rest)
end
end
end
@doc false
def split_aggregate_opts(opts) do
{left, right} = Keyword.split(opts, opt_keys())
right =
Keyword.put(right, :tenant, left[:tenant])
right =
case Keyword.fetch(left, :authorize?) do
{:ok, value} ->
Keyword.put(right, :authorize?, value)
:error ->
right
end
case Keyword.fetch(right, :action) do
{:ok, action} ->
{Keyword.put(left, :read_action, action), right}
:error ->
{left, right}
end
end
def default_value(:count), do: 0
def default_value(:first), do: nil
def default_value(:sum), do: nil
def default_value(:max), do: nil
def default_value(:min), do: nil
def default_value(:avg), do: nil
def default_value(:exists), do: nil
def default_value(:list), do: []
def default_value(:custom), do: nil
@doc false
def build_query(resource, _parent, nil), do: Ash.Query.new(resource)
def build_query(resource, parent, build_opts) when is_list(build_opts) do
cond do
build_opts[:limit] ->
Ash.Query.add_error(resource, "Cannot set limit on aggregate query")
build_opts[:offset] && build_opts[:offset] != 0 ->
Ash.Query.add_error(resource, "Cannot set offset on aggregate query")
true ->
{filter, build_opts} = Keyword.pop(build_opts, :filter)
case Ash.Query.build(resource, build_opts) do
%{valid?: true} = query ->
resource
|> build_query(parent, query)
|> Ash.Query.do_filter(filter, parent_stack: [parent])
%{valid?: false} = query ->
query
end
end
end
def build_query(_resource, _parent, %Ash.Query{} = query) do
cond do
query.limit ->
Ash.Query.add_error(query, "Cannot set limit on aggregate query")
query.offset && query.offset != 0 ->
Ash.Query.add_error(query, "Cannot set offset on aggregate query")
true ->
Ash.Query.unset(query, [:load, :limit, :offset])
end
end
@doc false
def kind_to_type({:custom, type}, _attribute_type, _attribute_constraints), do: {:ok, type, []}
def kind_to_type(kind, attribute_type, attribute_constraints)
when kind in [:first, :sum, :max, :min],
do: {:ok, attribute_type, attribute_constraints || []}
def kind_to_type(:list, attribute_type, attribute_constraints),
do: {:ok, {:array, attribute_type}, [items: attribute_constraints || []]}
def kind_to_type(kind, attribute_type, _attribute_constraints) do
with {:ok, type} <- kind_to_type(kind, attribute_type) do
{:ok, type, []}
end
end
defp kind_to_type({:custom, type}, _attribute_type), do: {:ok, type}
defp kind_to_type(:count, _attribute_type), do: {:ok, Ash.Type.Integer}
defp kind_to_type(:exists, _attribute_type), do: {:ok, Ash.Type.Boolean}
defp kind_to_type(kind, nil), do: {:error, "Must provide field type for #{kind}"}
defp kind_to_type(:avg, _attribute_type), do: {:ok, :float}
defp kind_to_type(kind, attribute_type) when kind in [:first, :sum, :max, :min],
do: {:ok, attribute_type}
defp kind_to_type(:list, attribute_type), do: {:ok, {:array, attribute_type}}
defp kind_to_type(kind, _attribute_type), do: {:error, "Invalid aggregate kind: #{kind}"}
defimpl Inspect do
import Inspect.Algebra
def inspect(%{query: nil} = aggregate, opts) do
container_doc(
"#" <> to_string(aggregate.kind) <> "<",
[Enum.join(aggregate.relationship_path, ".")],
">",
opts,
fn str, _ -> str end,
separator: ""
)
end
def inspect(%{query: query} = aggregate, opts) do
field =
if aggregate.field do
if is_atom(aggregate.field) do
[to_string(aggregate.field)]
else
case aggregate.field do
%{agg_name: agg_name} ->
[to_string(agg_name)]
%{calc_name: calc_name} ->
[to_string(calc_name)]
_ ->
[inspect(aggregate.field)]
end
end
else
[]
end
container_doc(
"#" <> to_string(aggregate.kind) <> "<",
[
concat([
Enum.join(aggregate.relationship_path ++ field, "."),
concat(" from ", to_doc(query, opts))
])
],
">",
opts,
fn str, _ -> str end
)
end
end
defp aggregate_type(resource, aggregate) do
attribute =
if aggregate.field do
related = Ash.Resource.Info.related(resource, aggregate.relationship_path)
Ash.Resource.Info.attribute(related, aggregate.field)
end
attribute_type =
if attribute do
attribute.type
end
attribute_constraints =
if attribute do
attribute.constraints
end
Ash.Query.Aggregate.kind_to_type(aggregate.kind, attribute_type, attribute_constraints)
end
end