Packages
ash
1.23.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/actions/side_load.ex
defmodule Ash.Actions.SideLoad do
@moduledoc false
alias Ash.Engine
alias Ash.Engine.Request
require Ash.Query
def requests(
query,
use_data_for_filter? \\ true,
root_data \\ [],
root_query \\ nil,
path \\ [],
tenant \\ nil
)
def requests(nil, _, _, _, _, _), do: []
def requests(%{side_load: []}, _, _, _root_query, _, _), do: []
def requests(
%{side_load: side_loads} = query,
use_data_for_filter?,
root_data,
root_query,
path,
tenant
) do
root_query = root_query || query
tenant = tenant || query.tenant || root_query.tenant || tenant_from_data(root_data)
side_loads
|> List.wrap()
|> Enum.flat_map(fn {relationship, further} ->
relationship = Ash.Resource.relationship(query.resource, relationship)
related_query =
case further do
%Ash.Query{} = query ->
Ash.Query.set_api(query, 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
new_path = [relationship | path]
requests(
related_query,
use_data_for_filter?,
root_data,
root_query,
new_path,
related_query.tenant
) ++
do_requests(
relationship,
related_query,
path,
root_query,
root_data,
use_data_for_filter?
)
end)
end
defp tenant_from_data([%{__metadata__: %{tenant: tenant}} | _]), do: tenant
defp tenant_from_data(_), do: nil
def attach_side_loads([%resource{} | _] = data, %{side_load: side_loads})
when is_list(data) do
side_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, 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_side_loads(
data,
lead_path,
last_relationship,
side_loads,
value
)
%{cardinality: :many} ->
attach_to_many_side_loads(value, last_relationship, data, lead_path)
%{cardinality: :one} ->
attach_to_one_side_loads(value, last_relationship, data, lead_path)
end
end)
end
def attach_side_loads(data, state) when not is_list(data) do
[data]
|> attach_side_loads(state)
|> List.first()
end
def attach_side_loads(data, _) do
data
end
defp attach_to_many_side_loads(value, last_relationship, data, lead_path) do
values = Enum.group_by(value, &Map.get(&1, last_relationship.destination_field))
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_field)
related_records = Map.get(values, source_key, [])
Map.put(record, last_relationship.name, related_records)
end)
end
defp attach_to_one_side_loads(value, last_relationship, data, lead_path) do
values =
Enum.into(value, %{}, fn item ->
{Map.get(item, last_relationship.destination_field), item}
end)
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_field)
related_record = Map.get(values, source_key)
Map.put(record, last_relationship.name, related_record)
end)
end
defp attach_many_to_many_side_loads(data, lead_path, last_relationship, side_loads, value) do
join_path = lead_path ++ [last_relationship.join_relationship]
join_data =
side_loads
|> Map.get(join_path, [])
map_or_update(data, lead_path, fn record ->
source_value = Map.get(record, last_relationship.source_field)
join_values =
join_data
|> Enum.filter(fn join_row ->
Map.get(join_row, last_relationship.source_field_on_join_table) ==
source_value
end)
|> Enum.map(&Map.get(&1, last_relationship.destination_field_on_join_table))
related_records =
value
|> Enum.filter(fn value ->
destination_value = Map.get(value, last_relationship.destination_field)
destination_value in join_values
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.relationship(resource, last) || raise "Assumption Failed"
end
defp last_relationship!(resource, [first | rest]) do
relationship = Ash.Resource.relationship(resource, first) || raise "Assumption Failed"
last_relationship!(relationship.destination, rest)
end
defp do_requests(relationship, related_query, path, root_query, root_data, use_data_for_filter?) do
side_load_request =
side_load_request(
relationship,
related_query,
root_query,
path,
root_data,
use_data_for_filter?
)
case relationship.type do
:many_to_many ->
case join_assoc_request(
relationship,
related_query,
root_query,
path,
root_data,
use_data_for_filter?
) do
nil ->
[side_load_request]
request ->
[side_load_request, request]
end
_ ->
[side_load_request]
end
end
defp side_load_request(
relationship,
related_query,
root_query,
path,
root_data,
use_data_for_filter?
) do
request_path = [
:side_load,
Enum.reverse(Enum.map([relationship | path], &Map.get(&1, :name)))
]
dependencies =
case path do
[] ->
[request_path ++ [:authorization_filter]]
dependent_path ->
[
request_path ++ [:authorization_filter],
[:side_load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :data]
]
end
dependencies =
if use_data_for_filter? do
[[:data, :data] | dependencies]
else
dependencies
end
dependencies =
if relationship.type == :many_to_many do
join_relationship = join_relationship(relationship)
[
[
:side_load,
Enum.map(join_relationship_path(path, join_relationship), & &1.name),
:data
]
| dependencies
]
else
dependencies
end
source =
[relationship | path]
|> Enum.reverse()
|> Enum.map_join(".", &Map.get(&1, :name))
Engine.Request.new(
action: Ash.Resource.primary_action!(relationship.destination, :read),
resource: relationship.destination,
name: "side_load #{source}",
api: related_query.api,
path: request_path,
query:
side_load_query(
relationship,
related_query,
path,
root_query,
use_data_for_filter?
),
data:
Request.resolve(dependencies, fn data ->
base_query =
case get_in(data, request_path ++ [:authorization_filter]) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
with {:ok, new_query} <-
true_side_load_query(
relationship,
base_query,
data,
path,
root_data,
use_data_for_filter?
),
{:ok, results} <-
run_actual_query(
root_data,
new_query,
data,
path,
use_data_for_filter?,
relationship
) do
{:ok, results}
else
{:error, error} -> {:error, error}
end
end)
)
end
defp join_relationship(relationship) do
Ash.Resource.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,
related_query,
root_query,
path,
root_data,
use_data_for_filter?
) 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
[[:side_load, join_relationship_path_names, :authorization_filter]]
else
[
[:side_load, join_relationship_path_names, :authorization_filter],
[:side_load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :data]
]
end
dependencies =
if use_data_for_filter? do
[[:data, :data] | dependencies]
else
dependencies
end
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
Request.new(
action: Ash.Resource.primary_action!(relationship.destination, :read),
resource: relationship.through,
name: "side_load join #{join_relationship.name}",
api: related_query.api,
path: [:side_load, join_relationship_path_names],
query:
side_load_query(
join_relationship,
related_query,
join_relationship_path,
root_query,
use_data_for_filter?
),
data:
Request.resolve(dependencies, fn data ->
base_query =
case get_in(data, [
:side_load,
join_relationship_path_names,
:authorization_filter
]) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
base_query =
if related_query.tenant do
Ash.Query.set_tenant(base_query, related_query.tenant)
else
base_query
end
with {:ok, new_query} <-
true_side_load_query(
join_relationship,
base_query,
data,
path,
root_data,
use_data_for_filter?
),
{:ok, results} <-
run_actual_query(
root_data,
new_query,
data,
path,
use_data_for_filter?,
relationship
) do
{:ok, results}
else
{:error, error} -> {:error, error}
end
end)
)
end
end
defp run_actual_query(root_data, query, data, path, use_data?, relationship) do
offset? = is_nil(query.offset) || query.offset != 0
source_data =
case path do
[] when use_data? ->
get_in(data, [:data, :data])
[] ->
root_data
path ->
data
|> Map.get(:side_load, %{})
|> Map.get(path, %{})
|> Map.get(:data, [])
end
cond do
(query.limit ||
offset?) && relationship.type != :many_to_many &&
Ash.Resource.data_layer_can?(
relationship.source,
{:lateral_join, relationship.destination}
) ->
query
|> Ash.Query.set_context(%{
data_layer: %{
lateral_join_source:
{source_data, relationship.source, relationship.source_field,
relationship.destination_field}
}
})
|> Ash.Actions.Read.unpaginated_read()
(query.limit || offset?) && relationship.type != :many_to_many ->
artificial_limit_and_offset(query, relationship)
true ->
Ash.Actions.Read.unpaginated_read(query)
end
end
defp artificial_limit_and_offset(query, relationship) do
case Ash.Actions.Read.unpaginated_read(query) do
{:ok, results} ->
new_results =
results
|> Enum.group_by(fn record ->
Map.get(record, relationship.destination_key)
end)
|> Enum.flat_map(fn {_, group} ->
offset_records = Enum.drop(group, query.offset || 0)
if query.limit do
Enum.take(offset_records, query.limit)
else
offset_records
end
end)
{:ok, new_results}
{:error, error} ->
{:error, error}
end
end
defp side_load_query_with_reverse_path(
root_query,
related_query,
reverse_path,
root_data_filter
) do
case Ash.Filter.parse(root_query.resource, root_query.filter) do
{:ok, parsed} ->
related_query
|> Ash.Query.unset(:side_load)
|> Ash.Query.filter(
^put_nested_relationship(
[],
reverse_path,
root_data_filter,
false
)
)
|> Ash.Query.filter(^put_nested_relationship([], reverse_path, parsed, false))
|> Ash.Query.filter(^related_query.filter)
|> extract_errors()
{:error, error} ->
{:error, error}
end
end
defp side_load_query(
relationship,
related_query,
path,
root_query,
true
) do
Request.resolve([[:data, :data]], fn %{data: %{data: data}} ->
root_data_filter =
case data do
[] ->
false
[%resource{} = item] ->
item
|> Map.take(Ash.Resource.primary_key(resource))
|> Enum.to_list()
[%resource{} | _] = items ->
pkey = Ash.Resource.primary_key(resource)
[or: Enum.map(items, fn item -> item |> Map.take(pkey) |> Enum.to_list() end)]
end
case reverse_relationship_path(relationship, Enum.drop(path, 1)) do
{:ok, reverse_path} ->
side_load_query_with_reverse_path(
root_query,
related_query,
reverse_path,
root_data_filter
)
_ ->
relationship.destination
|> Ash.Query.new(related_query.api)
|> Ash.Query.filter(^related_query.filter)
|> extract_errors()
end
end)
end
defp side_load_query(
relationship,
related_query,
path,
root_query,
false
) do
case reverse_relationship_path(relationship, path) do
{:ok, reverse_path} ->
case Ash.Filter.parse(root_query.resource, root_query.filter) do
{:ok, parsed} ->
related_query
|> Ash.Query.filter(
^put_nested_relationship(
[],
reverse_path,
parsed,
false
)
)
|> Ash.Query.unset(:side_load)
{:error, error} ->
{:error, error}
end
:error ->
related_query
|> Ash.Query.unset(:side_load)
end
end
defp extract_errors(%{errors: []} = item), do: {:ok, item}
defp extract_errors(%{errors: errors}), do: {:error, errors}
defp true_side_load_query(relationship, query, data, path, root_data, use_data?) do
{source_field, path} =
if relationship.type == :many_to_many do
join_relationship = join_relationship(relationship)
{relationship.destination_field_on_join_table,
join_relationship_path(path, join_relationship) |> Enum.map(& &1.name)}
else
{relationship.source_field, path |> Enum.reverse() |> Enum.map(& &1.name)}
end
source_data =
case path do
[] when use_data? ->
Map.get(data, :data)
[] ->
%{data: root_data}
path ->
data
|> Map.get(:side_load, %{})
|> Map.get(path, %{})
end
get_query(query, relationship, source_data, source_field)
end
defp get_query(query, relationship, source_data, source_field) do
offset? = is_nil(query.offset) || query.offset != 0
cond do
(query.limit ||
offset?) && relationship.type != :many_to_many &&
Ash.Resource.data_layer_can?(
relationship.source,
{:lateral_join, relationship.destination}
) ->
{:ok, Ash.Query.unset(query, :side_load)}
(query.limit || offset?) && relationship.type != :many_to_many ->
{:ok, Ash.Query.unset(query, [:side_load, :limit, :offset])}
true ->
related_data = Map.get(source_data || %{}, :data, [])
ids =
Enum.flat_map(related_data, fn data ->
data
|> Map.get(source_field)
|> List.wrap()
end)
filter_value =
case ids do
[id] ->
id
ids ->
[in: ids]
end
new_query =
query
|> Ash.Query.filter(^[{relationship.destination_field, filter_value}])
{:ok, new_query}
end
end
def reverse_relationship_path(relationship, prior_path, acc \\ [])
def reverse_relationship_path(relationship, [], acc) do
relationship.destination
|> Ash.Resource.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
{:ok, Enum.reverse([reverse.name | acc])}
end
end
def reverse_relationship_path(relationship, [next_relationship | rest], acc) do
relationship.destination
|> Ash.Resource.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
reverse_relationship_path(next_relationship, rest, [reverse.name | acc])
end
end
defp reverse_relationship?(rel, destination_rel) do
rel.source == destination_rel.destination &&
rel.source_field == destination_rel.destination_field &&
rel.destination_field == destination_rel.source_field &&
Map.fetch(rel, :source_field_on_join_table) ==
Map.fetch(destination_rel, :destination_field_on_join_table) &&
Map.fetch(rel, :destination_field_on_join_table) ==
Map.fetch(destination_rel, :source_field_on_join_table)
end
defp put_nested_relationship(request_filter, path, value, records?) when not is_list(value) do
put_nested_relationship(request_filter, path, [value], records?)
end
defp put_nested_relationship(request_filter, [rel | rest], values, records?) do
[
{rel, put_nested_relationship(request_filter, rest, values, records?)}
]
end
defp put_nested_relationship(request_filter, [], [{field, value}], _) do
[{field, value} | request_filter]
end
defp put_nested_relationship(request_filter, [], [{field, _} | _] = keys, _) do
[{field, [{:in, Enum.map(keys, &elem(&1, 1))}]} | request_filter]
end
defp put_nested_relationship(request_filter, [], [values], _) do
List.wrap(request_filter) ++ List.wrap(values)
end
defp put_nested_relationship(request_filter, [], values, _) do
Keyword.update(request_filter, :or, values, &Kernel.++(&1, values))
end
end