Skip to content

Commit 4e24d0f

Browse files
authored
feat: manual relationship via list (#2546)
Introduces the ability to return a list from the `Ash.Resource.ManualRelationship.load/3` callback. It works similarly to the `Ash.Resource.Calculation.calculate/3` callback: Each result in the returned list is related to the record from the same position in the input records list. The ability to return a map is maintained for backwards compatibility .
1 parent 3f8ad9c commit 4e24d0f

File tree

3 files changed

+453
-95
lines changed

3 files changed

+453
-95
lines changed

lib/ash/actions/read/relationships.ex

Lines changed: 130 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -505,12 +505,14 @@ defmodule Ash.Actions.Read.Relationships do
505505
tenant: related_query.tenant
506506
})
507507
|> case do
508-
{:ok, records} ->
509-
records
510-
|> Enum.flat_map(fn {key, value} ->
508+
{:ok, result_records} ->
509+
result_records
510+
|> normalize_manual_results(records, relationship)
511+
|> Enum.with_index()
512+
|> Enum.flat_map(fn {value, index} ->
511513
value
512514
|> List.wrap()
513-
|> Enum.map(&Ash.Resource.put_metadata(&1, :manual_key, key))
515+
|> Enum.map(&Ash.Resource.put_metadata(&1, :manual_key, index))
514516
end)
515517
|> Ash.load(related_query,
516518
domain: related_query.domain,
@@ -520,7 +522,7 @@ defmodule Ash.Actions.Read.Relationships do
520522
)
521523
|> case do
522524
{:ok, results} ->
523-
{:ok, regroup_manual_results(results, relationship)}
525+
{:ok, regroup_manual_results(results, relationship, Enum.count(records))}
524526

525527
{:error, error} ->
526528
{:error, error}
@@ -870,18 +872,126 @@ defmodule Ash.Actions.Read.Relationships do
870872
end)
871873
end
872874

873-
defp regroup_manual_results(records, %{cardinality: :many}) do
874-
Enum.group_by(records, & &1.__metadata__.manual_key, &delete_manual_key/1)
875+
defp regroup_manual_results(records, %{cardinality: :many}, count) do
876+
indexed_records =
877+
Enum.group_by(records, & &1.__metadata__.manual_key, &delete_manual_key/1)
878+
879+
Enum.map(0..max(count - 1, 0), fn index ->
880+
Map.get(indexed_records, index, [])
881+
end)
875882
end
876883

877-
defp regroup_manual_results(records, %{cardinality: :one}) do
878-
Map.new(records, &{&1.__metadata__.manual_key, delete_manual_key(&1)})
884+
defp regroup_manual_results(records, %{cardinality: :one}, count) do
885+
indexed_records =
886+
Map.new(records, fn record ->
887+
{record.__metadata__.manual_key, delete_manual_key(record)}
888+
end)
889+
890+
Enum.map(0..max(count - 1, 0), fn index ->
891+
Map.get(indexed_records, index)
892+
end)
879893
end
880894

881895
defp delete_manual_key(record) do
882896
Map.update!(record, :__metadata__, &Map.delete(&1, :manual_key))
883897
end
884898

899+
defp normalize_manual_results(result_records, [%resource{} | _] = records, relationship)
900+
when is_map(result_records) do
901+
default =
902+
case relationship.cardinality do
903+
:one ->
904+
nil
905+
906+
:many ->
907+
[]
908+
end
909+
910+
if Ash.Resource.Info.primary_key_simple_equality?(resource) do
911+
pkey = Ash.Resource.Info.primary_key(resource)
912+
913+
single_match? =
914+
case pkey do
915+
[_] -> true
916+
_ -> false
917+
end
918+
919+
Enum.map(records, fn record ->
920+
value =
921+
if single_match? do
922+
case Map.fetch(result_records, Map.get(record, Enum.at(pkey, 0))) do
923+
{:ok, value} -> {:ok, value}
924+
:error -> Map.fetch(result_records, Map.take(record, pkey))
925+
end
926+
else
927+
Map.fetch(result_records, Map.take(record, pkey))
928+
end
929+
930+
case value do
931+
{:ok, result} ->
932+
result
933+
934+
:error ->
935+
default
936+
end
937+
end)
938+
else
939+
pkey = Ash.Resource.Info.primary_key(resource)
940+
941+
case pkey do
942+
[pkey_key] ->
943+
Enum.map(records, fn record ->
944+
pkey_values = Map.take(record, pkey)
945+
946+
value =
947+
Enum.find_value(result_records, fn {key, value} ->
948+
if is_map(key) do
949+
if resource.primary_key_matches?(key, pkey_values) do
950+
{:ok, value}
951+
end
952+
else
953+
if resource.primary_key_matches?(%{pkey_key => key}, pkey_values) do
954+
{:ok, value}
955+
end
956+
end
957+
end) || :error
958+
959+
case value do
960+
{:ok, result} ->
961+
result
962+
963+
:error ->
964+
default
965+
end
966+
end)
967+
968+
_pkeys ->
969+
Enum.map(records, fn record ->
970+
pkey_values = Map.take(record, pkey)
971+
972+
value =
973+
Enum.find_value(result_records, fn {key, value} ->
974+
if resource.primary_key_matches?(key, pkey_values) do
975+
{:ok, value}
976+
end
977+
end) || :error
978+
979+
case value do
980+
{:ok, result} ->
981+
result
982+
983+
:error ->
984+
default
985+
end
986+
end)
987+
end
988+
end
989+
end
990+
991+
defp normalize_manual_results(result_records, _records, _relationship) do
992+
result_records
993+
end
994+
885995
defp select_destination_attribute(related_query, relationship) do
886996
if Map.get(relationship, :no_attributes?) ||
887997
(Map.get(relationship, :manual) &&
@@ -1082,99 +1192,25 @@ defmodule Ash.Actions.Read.Relationships do
10821192
end
10831193

10841194
defp do_attach_related_records(
1085-
[%resource{} | _] = records,
1195+
records,
10861196
%{manual: {_module, _opts}} = relationship,
1087-
map,
1197+
values,
10881198
_related_query
10891199
) do
10901200
default =
10911201
case relationship.cardinality do
1092-
:one ->
1093-
nil
1094-
1095-
:many ->
1096-
[]
1202+
:one -> nil
1203+
:many -> []
10971204
end
10981205

1099-
if Ash.Resource.Info.primary_key_simple_equality?(resource) do
1100-
pkey = Ash.Resource.Info.primary_key(resource)
1101-
1102-
single_match? =
1103-
case pkey do
1104-
[_] -> true
1105-
_ -> false
1106-
end
1107-
1108-
Enum.map(records, fn record ->
1109-
value =
1110-
if single_match? do
1111-
case Map.fetch(map, Map.get(record, Enum.at(pkey, 0))) do
1112-
{:ok, value} -> {:ok, value}
1113-
:error -> Map.fetch(map, Map.take(record, pkey))
1114-
end
1115-
else
1116-
Map.fetch(map, Map.take(record, pkey))
1117-
end
1118-
1119-
case value do
1120-
{:ok, result} ->
1121-
Map.put(record, relationship.name, result)
1122-
1123-
:error ->
1124-
Map.put(record, relationship.name, default)
1125-
end
1126-
end)
1127-
else
1128-
pkey = Ash.Resource.Info.primary_key(resource)
1129-
1130-
case pkey do
1131-
[pkey_key] ->
1132-
Enum.map(records, fn record ->
1133-
pkey_values = Map.take(record, pkey)
1134-
1135-
value =
1136-
Enum.find_value(map, fn {key, value} ->
1137-
if is_map(key) do
1138-
if resource.primary_key_matches?(key, pkey_values) do
1139-
{:ok, value}
1140-
end
1141-
else
1142-
if resource.primary_key_matches?(%{pkey_key => key}, pkey_values) do
1143-
{:ok, value}
1144-
end
1145-
end
1146-
end) || :error
1147-
1148-
case value do
1149-
{:ok, result} ->
1150-
Map.put(record, relationship.name, result)
1151-
1152-
:error ->
1153-
Map.put(record, relationship.name, default)
1154-
end
1155-
end)
1156-
1157-
_pkeys ->
1158-
Enum.map(records, fn record ->
1159-
pkey_values = Map.take(record, pkey)
1160-
1161-
value =
1162-
Enum.find_value(map, fn {key, value} ->
1163-
if resource.primary_key_matches?(key, pkey_values) do
1164-
{:ok, value}
1165-
end
1166-
end) || :error
1167-
1168-
case value do
1169-
{:ok, result} ->
1170-
Map.put(record, relationship.name, result)
1206+
records
1207+
|> Enum.zip_with(values, fn
1208+
record, nil ->
1209+
Map.put(record, relationship.name, default)
11711210

1172-
:error ->
1173-
Map.put(record, relationship.name, default)
1174-
end
1175-
end)
1176-
end
1177-
end
1211+
record, value ->
1212+
Map.put(record, relationship.name, value)
1213+
end)
11781214
end
11791215

11801216
defp do_attach_related_records(

lib/ash/resource/manual_relationship/manual_relationship.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule Ash.Resource.ManualRelationship do
3939
opts :: Keyword.t(),
4040
context :: Context.t()
4141
) ::
42-
{:ok, map} | {:error, term}
42+
{:ok, [term] | map} | {:error, term}
4343

4444
defmacro __using__(_) do
4545
quote do

0 commit comments

Comments
 (0)