Packages

ash

2.0.0-rc.15
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
ash lib ash actions load.ex
Raw

lib/ash/actions/load.ex

defmodule Ash.Actions.Load do
@moduledoc false
alias Ash.Engine.Request
require Ash.Query
def requests(
query,
lazy?,
opts,
request_path,
root_query \\ nil,
path \\ [],
tenant \\ nil
)
def requests(nil, _, _opts, _, _, _, _), do: []
def requests(%{load: []}, _, _opts, _, _, _, _), do: []
def requests(
%{load: loads} = query,
lazy?,
opts,
request_path,
root_query,
path,
tenant
) do
root_query = root_query || query
tenant = tenant || query.tenant || root_query.tenant
loads
|> List.wrap()
|> Enum.flat_map(fn {relationship, further} ->
relationship = Ash.Resource.Info.relationship(query.resource, relationship)
related_query =
case further do
%Ash.Query{} = query ->
Ash.Query.set_api(query, relationship.api || root_query.api)
further ->
relationship.destination
|> Ash.Query.new(root_query.api)
|> Ash.Query.load(further)
end
related_query =
if tenant && !related_query.tenant do
Ash.Query.set_tenant(related_query, tenant)
else
related_query
end
|> Ash.Query.ensure_selected(relationship.destination_attribute)
related_query =
if relationship.cardinality == :one do
Ash.Query.limit(related_query, 1)
else
related_query
end
new_path = [relationship | path]
requests(
related_query,
lazy?,
opts,
request_path,
root_query,
new_path,
related_query.tenant
) ++
do_requests(
relationship,
lazy?,
opts,
request_path,
related_query,
path,
root_query
)
end)
end
def attach_loads([%resource{} | _] = data, loads) do
loads
|> Enum.sort_by(fn {key, _value} ->
last_relationship = last_relationship!(resource, key)
# We want to do `many_to_many` last, so we know we've
# done their join assocs first. Pretty hacky
{length(key), last_relationship.type == :many_to_many}
end)
|> Enum.reduce(data, fn {key, %{data: value}}, data ->
last_relationship = last_relationship!(resource, key)
lead_path = :lists.droplast(key)
case last_relationship do
%{type: :many_to_many} ->
attach_many_to_many_loads(
data,
lead_path,
last_relationship,
loads,
value
)
%{cardinality: :many} ->
attach_to_many_loads(value, last_relationship, data, lead_path)
%{cardinality: :one} ->
attach_to_one_loads(value, last_relationship, data, lead_path)
end
end)
end
def attach_loads(data, state) when not is_list(data) do
[data]
|> attach_loads(state)
|> List.first()
end
def attach_loads(data, _state) do
data
end
defp attach_to_many_loads(value, %{name: name, no_attributes?: true}, data, lead_path) do
map_or_update(data, lead_path, fn record ->
Map.put(record, name, List.wrap(value))
end)
end
defp attach_to_many_loads(value, last_relationship, data, lead_path) when is_map(value) do
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
value = Map.to_list(value)
single_primary_key =
case primary_key do
[field] ->
field
_ ->
nil
end
map_or_update(data, lead_path, fn record ->
related =
value
|> Enum.find_value(fn {key, value} ->
key =
if is_map(key) || !single_primary_key do
key
else
%{single_primary_key => key}
end
if last_relationship.source.primary_key_matches?(record, key) do
value
end
end)
Map.put(
record,
last_relationship.name,
related
)
end)
end
defp attach_to_many_loads(value, last_relationship, data, lead_path) do
destination_attribute = last_relationship.destination_attribute
type = Ash.Resource.Info.attribute(last_relationship.destination, destination_attribute).type
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_attribute)
related_records =
Enum.filter(value, fn maybe_related ->
Ash.Type.equal?(
type,
Map.get(maybe_related, destination_attribute),
source_key
)
end)
Map.put(record, last_relationship.name, related_records)
end)
end
defp attach_to_one_loads(value, %{name: name, no_attributes?: true}, data, lead_path) do
map_or_update(data, lead_path, fn record ->
Map.put(record, name, value |> List.wrap() |> Enum.at(0))
end)
end
defp attach_to_one_loads(value, last_relationship, data, lead_path) when is_map(value) do
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
single_primary_key =
case primary_key do
[field] ->
field
_ ->
nil
end
map_or_update(data, lead_path, fn record ->
related =
Enum.filter(value, fn {key, _value} ->
key =
if is_map(key) || !single_primary_key do
key
else
%{single_primary_key => key}
end
if last_relationship.source.primary_key_matches?(record, key) do
value
end
end)
|> List.wrap()
Map.put(
record,
last_relationship.name,
related
)
end)
end
defp attach_to_one_loads(value, last_relationship, data, lead_path) do
destination_attribute = last_relationship.destination_attribute
type = Ash.Resource.Info.attribute(last_relationship.destination, destination_attribute).type
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_attribute)
related_record =
Enum.find(value, fn maybe_related ->
Ash.Type.equal?(
type,
Map.get(maybe_related, destination_attribute),
source_key
)
end)
Map.put(record, last_relationship.name, related_record)
end)
end
defp attach_many_to_many_loads(data, lead_path, last_relationship, loads, value) do
join_path = lead_path ++ [last_relationship.join_relationship]
join_data =
loads
|> Map.get(join_path, %{})
|> Map.get(:data, [])
source_attribute_on_join_resource_type =
Ash.Resource.Info.attribute(
last_relationship.through,
last_relationship.source_attribute_on_join_resource
).type
destination_attribute_on_join_resource_type =
Ash.Resource.Info.attribute(
last_relationship.through,
last_relationship.destination_attribute_on_join_resource
).type
map_or_update(data, lead_path, fn record ->
source_value = Map.get(record, last_relationship.source_attribute)
join_values =
join_data
|> Enum.filter(fn join_row ->
Ash.Type.equal?(
source_attribute_on_join_resource_type,
Map.get(join_row, last_relationship.source_attribute_on_join_resource),
source_value
)
end)
|> Enum.map(&Map.get(&1, last_relationship.destination_attribute_on_join_resource))
related_records =
value
|> Enum.filter(fn value ->
destination_value = Map.get(value, last_relationship.destination_attribute)
Enum.any?(join_values, fn join_value ->
Ash.Type.equal?(
destination_attribute_on_join_resource_type,
destination_value,
join_value
)
end)
end)
Map.put(record, last_relationship.name, related_records)
end)
end
defp map_or_update(nil, _, _), do: nil
defp map_or_update(record, [], func) when not is_list(record), do: func.(record)
defp map_or_update(records, [], func) do
Enum.map(records, fn record ->
if record do
func.(record)
else
nil
end
end)
end
defp map_or_update(records, [path | tail], func) do
map_or_update(records, [], fn record ->
Map.update!(record, path, &map_or_update(&1, tail, func))
end)
end
defp last_relationship!(resource, [last]) do
Ash.Resource.Info.relationship(resource, last) || raise "Assumption Failed"
end
defp last_relationship!(resource, [first | rest]) do
relationship = Ash.Resource.Info.relationship(resource, first) || raise "Assumption Failed"
last_relationship!(relationship.destination, rest)
end
defp do_requests(relationship, lazy?, opts, request_path, related_query, path, root_query) do
load_request =
load_request(
relationship,
lazy?,
opts,
request_path,
related_query,
root_query,
path
)
case relationship.type do
:many_to_many ->
case join_assoc_request(
relationship,
request_path,
related_query,
root_query,
path,
opts,
lazy?
) do
nil ->
[load_request]
request ->
[load_request, request]
end
_ ->
[load_request]
end
end
defp load_request(
relationship,
lazy?,
opts,
request_path,
related_query,
root_query,
path
) do
relationship_path = Enum.reverse(Enum.map([relationship | path], &Map.get(&1, :name)))
this_request_path =
request_path ++
[
:load,
relationship_path
]
dependencies =
case path do
[] ->
[this_request_path ++ [:authorization_filter]]
dependent_path ->
[
this_request_path ++ [:authorization_filter],
request_path ++
[:load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :data],
request_path ++
[:load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :query]
]
end
dependencies = [request_path ++ [:data] | dependencies]
dependencies =
if relationship.type == :many_to_many &&
!lateral_join?(related_query, relationship, :unknown) do
join_relationship = join_relationship(relationship)
join_relationship_path =
Enum.map(join_relationship_path(path, join_relationship), & &1.name)
[
request_path ++
[
:load,
join_relationship_path,
:data
]
| dependencies
]
else
dependencies
end
source =
[relationship | path]
|> Enum.reverse()
|> Enum.map_join(".", &Map.get(&1, :name))
Request.new(
action:
related_query.action || Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.destination,
name: "load #{source}",
api: related_query.api,
path: this_request_path,
query:
load_query(
relationship,
related_query
),
data:
data(
relationship,
lazy?,
dependencies,
this_request_path,
request_path,
related_query,
path,
root_query,
opts
)
)
end
defp data(
%{manual: manual} = relationship,
lazy?,
dependencies,
_this_request_path,
request_path,
related_query,
path,
root_query,
request_opts
)
when not is_nil(manual) do
{mod, opts} =
case manual do
{mod, opts} ->
{mod, opts}
mod ->
{mod, []}
end
Request.resolve(dependencies, fn data ->
data =
case path do
[] ->
get_in(data, request_path ++ [:data, :results])
path ->
data
|> get_in(request_path)
|> Kernel.||(%{})
|> Map.get(:load, %{})
|> Map.get(Enum.reverse(Enum.map(path, & &1.name)), %{})
|> Map.get(:data, %{})
end
lazy_load_or(
data,
lazy?,
relationship.name,
related_query.api,
related_query,
request_opts,
fn ->
mod.load(data, opts, %{
relationship: relationship,
query: related_query,
root_query: root_query,
actor: request_opts[:actor],
authorize?: request_opts[:authorize?],
api: related_query.api,
tenant: related_query.tenant
})
end
)
end)
end
defp data(
relationship,
lazy?,
dependencies,
this_request_path,
request_path,
related_query,
path,
root_query,
opts
) do
Request.resolve(dependencies, fn data ->
base_query =
case get_in(data, this_request_path ++ [:authorization_filter]) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
source_query =
case path do
[] ->
root_query
path ->
get_in(
data,
request_path ++
[
:load,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))),
:query
]
)
end
source_query =
if related_query.tenant do
Ash.Query.set_tenant(source_query, related_query.tenant)
else
source_query
end
with {:ok, new_query} <-
true_load_query(
relationship,
base_query,
data,
path,
request_path
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
relationship,
source_query,
request_path,
opts,
lazy?
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
end
defp lazy_load_or(data, lazy?, relationship, api, related_query, request_opts, func) do
if lazy? && Ash.Resource.loaded?(data, relationship) do
pkey = Ash.Resource.Info.primary_key(related_query.resource)
data
|> get_related(relationship)
|> Enum.uniq_by(&Map.take(&1, pkey))
|> api.load(related_query,
lazy?: true,
authorize?: request_opts[:authorize?],
actor: request_opts[:actor]
)
else
func.()
end
end
defp get_related(data, relationship) when is_list(data) do
Enum.flat_map(data, fn item ->
item
|> Map.get(relationship)
|> List.wrap()
end)
end
defp get_related(data, relationship) do
get_related(List.wrap(data), relationship)
end
defp join_relationship(relationship) do
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
end
defp join_relationship_path(path, join_relationship) do
Enum.reverse([join_relationship | path])
end
defp join_assoc_request(
relationship,
request_path,
related_query,
root_query,
path,
opts,
lazy?
) do
join_relationship = join_relationship(relationship)
join_relationship_name = join_relationship.name
case path do
[%{name: ^join_relationship_name} | _] ->
nil
path ->
join_relationship_path = join_relationship_path(path, join_relationship)
join_relationship_path_names = Enum.map(join_relationship_path, & &1.name)
dependencies =
if path == [] do
[
[:load, join_relationship_path_names, :authorization_filter]
]
else
[
[:load, join_relationship_path_names, :authorization_filter],
[:load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :data],
[:load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :query]
]
end
lateral_join? = lateral_join?(related_query, relationship, :unknown)
dependencies =
if lateral_join? do
dependencies ++
[
[
:load,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))) ++ [relationship.name],
:data
]
]
else
dependencies
end
dependencies = [[:data] | dependencies]
related_query =
if related_query.tenant do
join_relationship.destination
|> Ash.Query.new(related_query.api)
|> Ash.Query.set_tenant(related_query.tenant)
else
Ash.Query.new(join_relationship.destination, related_query.api)
end
dependencies = Enum.map(dependencies, &(request_path ++ &1))
Request.new(
action:
related_query.action ||
Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.through,
name: "load join #{join_relationship.name}",
api: related_query.api,
path: request_path ++ [:load, join_relationship_path_names],
query:
load_query(
join_relationship,
related_query
),
data:
Request.resolve(dependencies, fn
data ->
base_query =
case get_in(
data,
request_path ++
[
:load,
join_relationship_path_names,
:authorization_filter
]
) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
source_data =
case path do
[] ->
get_in(data, request_path ++ [:data, :results])
path ->
data
|> get_in(request_path)
|> Kernel.||(%{})
|> Map.get(:load, %{})
|> Map.get(Enum.reverse(Enum.map(path, & &1.name)), %{})
|> Map.get(:data, %{})
end
lateral_join? = lateral_join?(related_query, relationship, source_data)
base_query =
if related_query.tenant do
Ash.Query.set_tenant(base_query, related_query.tenant)
else
base_query
end
source_query =
case path do
[] ->
root_query
path ->
get_in(
data,
request_path ++
[:load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :query]
)
end
source_query =
if related_query.tenant do
Ash.Query.set_tenant(source_query, related_query.tenant)
else
source_query
end
with {:ok, new_query} <-
true_load_query(
join_relationship,
base_query,
data,
path,
request_path
),
new_query <-
add_join_destination_filter(
new_query,
lateral_join?,
data,
relationship,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))) ++ [relationship.name]
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
join_relationship,
source_query,
request_path,
opts,
lazy?
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
)
end
end
defp add_join_destination_filter(query, true, data, relationship, destination_path) do
ids =
data
|> get_in([
:load,
destination_path,
:data
])
|> case do
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
results
data ->
data
end
|> List.wrap()
|> Enum.map(fn related ->
Map.get(related, relationship.destination_attribute)
end)
filter = [{relationship.destination_attribute_on_join_resource, [{:in, ids}]}]
Ash.Query.filter(query, ^filter)
end
defp add_join_destination_filter(query, false, _, _, _) do
query
end
defp lateral_join?(query, relationship, source_data) do
action =
case relationship.read_action do
nil ->
Ash.Resource.Info.primary_action!(relationship.destination, :read)
action ->
Ash.Resource.Info.action(relationship.destination, action)
end
if action.manual do
false
else
{offset, limit} = offset_and_limit(query)
resources =
[relationship.source, Map.get(relationship, :through), relationship.destination]
|> Enum.reject(&is_nil/1)
cond do
limit == 1 && is_nil(relationship.context) && is_nil(relationship.filter) &&
is_nil(relationship.sort) && relationship.type != :many_to_many ->
false
limit == 1 && (source_data == :unknown || Enum.count_until(source_data, 2) == 1) &&
relationship.type != :many_to_many ->
false
true ->
lateral_join =
(limit || offset) &&
Ash.DataLayer.data_layer_can?(
relationship.source,
{:lateral_join, resources}
)
!!lateral_join
end
end
end
defp run_actual_query(
query,
base_query,
data,
path,
relationship,
source_query,
request_path,
request_opts,
lazy?
) do
{offset, limit} = offset_and_limit(base_query)
source_data =
case path do
[] ->
get_in(data, request_path ++ [:data, :results])
path ->
data
|> get_in(request_path)
|> Kernel.||(%{})
|> Map.get(:load, %{})
|> Map.get(Enum.reverse(Enum.map(path, & &1.name)), %{})
|> Map.get(:data, %{})
end
lazy_load_or(
source_data,
lazy?,
relationship.name,
query.api,
query,
request_opts,
fn ->
cond do
lateral_join?(query, relationship, source_data) && relationship.type == :many_to_many ->
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
query
|> Ash.Query.set_context(%{
data_layer: %{
lateral_join_source: {
source_data,
[
{source_query, relationship.source_attribute,
relationship.source_attribute_on_join_resource, relationship},
{relationship.through, relationship.destination_attribute_on_join_resource,
relationship.destination_attribute, join_relationship}
]
}
}
})
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action, request_opts)
lateral_join?(query, relationship, source_data) && (limit || offset) ->
query
|> Ash.Query.set_context(%{
data_layer: %{
lateral_join_source:
{source_data,
[
{source_query, relationship.source_attribute,
relationship.destination_attribute, relationship}
]}
}
})
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action, request_opts)
limit || offset ->
artificial_limit_and_offset(query, limit, offset, relationship, request_opts)
true ->
query
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action, request_opts)
end
end
)
end
defp offset_and_limit(query) do
if query.offset == 0 do
{nil, query.limit}
else
{query.offset, query.limit}
end
end
defp artificial_limit_and_offset(query, limit, offset, relationship, request_opts) do
query
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action, request_opts)
|> case do
{:ok, results} ->
new_results =
results
|> Enum.group_by(fn record ->
Map.get(record, relationship.destination_attribute)
end)
|> Enum.flat_map(fn {_, group} ->
offset_records = Enum.drop(group, offset || 0)
if limit do
Enum.take(offset_records, limit)
else
offset_records
end
end)
{:ok, new_results}
{:error, error} ->
{:error, error}
end
end
defp read(query, action, request_opts) do
action = action || primary_read(query)
Ash.Actions.Read.unpaginated_read(query, action, request_opts)
end
defp primary_read(query) do
action = Ash.Resource.Info.primary_action(query.resource, :read)
if action do
action
else
raise """
No read action for loaded resource: #{query.resource}
"""
end
end
defp remove_relationships_from_load(query) do
case query.load do
empty when empty in [nil, []] ->
query
load ->
new_load =
load
|> List.wrap()
|> Enum.reject(fn
item when is_atom(item) ->
Ash.Resource.Info.relationship(query.resource, item)
{item, _} ->
Ash.Resource.Info.relationship(query.resource, item)
end)
%{query | load: new_load}
end
end
defp load_query(%{manual: manual}, related_query)
when not is_nil(manual) do
related_query
end
defp load_query(
relationship,
related_query
) do
if Map.get(relationship, :no_attributes?) do
relationship.destination
|> Ash.Query.new(related_query.api)
else
relationship.destination
|> Ash.Query.new(related_query.api)
|> Ash.Query.filter(^related_query.filter)
end
end
defp true_load_query(relationship, query, data, path, request_path) do
{source_attribute, path} =
if relationship.type == :many_to_many do
join_relationship = join_relationship(relationship)
{relationship.destination_attribute_on_join_resource,
join_relationship_path(path, join_relationship) |> Enum.map(& &1.name)}
else
{relationship.source_attribute, path |> Enum.reverse() |> Enum.map(& &1.name)}
end
source_data =
case path do
[] ->
get_in(data, request_path ++ [:data, :results])
path ->
data
|> get_in(request_path)
|> Kernel.||(%{})
|> Map.get(:load, %{})
|> Map.get(path, %{})
|> Map.get(:data, %{})
end
case source_data do
%{data: empty} when empty in [[], nil] ->
:nothing
empty when empty in [[], nil] ->
:nothing
_ ->
get_query(query, relationship, source_data, source_attribute)
end
end
defp get_query(query, relationship, source_data, source_attribute) do
{offset, limit} = offset_and_limit(query)
cond do
lateral_join?(query, relationship, source_data) ->
{:ok, Ash.Query.unset(query, :load)}
Map.get(relationship, :no_attributes?) ->
{:ok, query}
true ->
query =
if limit || offset do
Ash.Query.unset(query, [:limit, :offset])
else
query
end
related_data =
case source_data do
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
results
data ->
data
end
ids =
Enum.flat_map(related_data, fn data ->
data
|> Map.get(source_attribute)
|> List.wrap()
end)
filter_value =
case ids do
[id] ->
id
ids ->
[in: ids]
end
new_query =
query
|> Ash.Query.filter(^[{relationship.destination_attribute, filter_value}])
|> Ash.Query.unset(:load)
{:ok, new_query}
end
end
def reverse_relationship_path(relationship, []) do
relationship.destination
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
{:ok, [reverse.name]}
end
end
def reverse_relationship_path(relationship, rest) do
relationship.destination
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
case do_reverse_relationship_path(reverse, rest) do
:error ->
:error
{:ok, path} ->
{:ok, [reverse.name | path]}
end
end
end
def do_reverse_relationship_path(relationship, prior_path, acc \\ [])
def do_reverse_relationship_path(nil, _, _) do
:error
end
def do_reverse_relationship_path(relationship, [], acc) do
relationship.source
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
{:ok, [reverse.name | acc]}
end
end
def do_reverse_relationship_path(relationship, [next_relationship | rest], acc) do
relationship.destination
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
do_reverse_relationship_path(
Ash.Resource.Info.relationship(relationship.destination, next_relationship),
rest,
[reverse.name | acc]
)
end
end
defp reverse_relationship?(rel, destination_rel) do
rel.source == destination_rel.destination &&
rel.destination == destination_rel.source &&
rel.source_attribute == destination_rel.destination_attribute &&
rel.destination_attribute == destination_rel.source_attribute &&
Map.fetch(rel, :source_attribute_on_join_resource) ==
Map.fetch(destination_rel, :destination_attribute_on_join_resource) &&
Map.fetch(rel, :destination_attribute_on_join_resource) ==
Map.fetch(destination_rel, :source_attribute_on_join_resource) &&
is_nil(destination_rel.context) &&
is_nil(rel.context)
end
end