Skip to content

Commit 08238e4

Browse files
committed
Fix relationship issue 28
1 parent 081a217 commit 08238e4

5 files changed

Lines changed: 199 additions & 23 deletions

File tree

lib/ash_json_api/controllers/helpers.ex

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ defmodule AshJsonApi.Controllers.Helpers do
458458
def add_to_relationship(request, relationship_name) do
459459
chain(request, fn %{assigns: %{result: result}} ->
460460
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
461-
values = normalize_relationship_identifiers(request)
461+
values = relationship_change_values(request, relationship_name)
462462

463463
result
464464
|> Ash.Changeset.new()
@@ -469,8 +469,24 @@ defmodule AshJsonApi.Controllers.Helpers do
469469
|> Ash.update(Request.opts(request))
470470
|> case do
471471
{:ok, updated} ->
472+
identifier_meta =
473+
case request.resource_identifiers do
474+
list when is_list(list) ->
475+
Enum.reduce(list, %{}, fn
476+
{%{id: id}, meta}, acc when is_map(meta) ->
477+
Map.put(acc, id, meta)
478+
479+
_, acc ->
480+
acc
481+
end)
482+
483+
_ ->
484+
%{}
485+
end
486+
472487
request
473488
|> Request.assign(:result, Map.get(updated, relationship_name))
489+
|> Request.assign(:relationship_identifier_meta, identifier_meta)
474490

475491
{:error, error} ->
476492
Request.add_error(request, error, :add_to_relationship)
@@ -481,7 +497,7 @@ defmodule AshJsonApi.Controllers.Helpers do
481497
def replace_relationship(request, relationship_name) do
482498
chain(request, fn %{assigns: %{result: result}} ->
483499
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
484-
values = normalize_relationship_identifiers(request)
500+
values = relationship_change_values(request, relationship_name)
485501

486502
result
487503
|> Ash.Changeset.new()
@@ -504,7 +520,7 @@ defmodule AshJsonApi.Controllers.Helpers do
504520
def delete_from_relationship(request, relationship_name) do
505521
chain(request, fn %{assigns: %{result: result}} ->
506522
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
507-
values = normalize_relationship_identifiers(request)
523+
values = relationship_change_values(request, relationship_name)
508524

509525
result
510526
|> Ash.Changeset.new()
@@ -1089,4 +1105,45 @@ defmodule AshJsonApi.Controllers.Helpers do
10891105
id
10901106
end
10911107
end
1108+
1109+
defp relationship_change_values(request, relationship_name) do
1110+
relationship = Ash.Resource.Info.relationship(request.resource, relationship_name)
1111+
1112+
meta_mapping =
1113+
AshJsonApi.Resource.Info.relationship_meta_mapping(request.resource, relationship_name)
1114+
1115+
if match?(%Ash.Resource.Relationships.ManyToMany{}, relationship) and meta_mapping != [] do
1116+
build_many_to_many_values(request.resource_identifiers, relationship, meta_mapping)
1117+
else
1118+
normalize_relationship_identifiers(request)
1119+
end
1120+
end
1121+
1122+
defp build_many_to_many_values(nil, _relationship, _meta_mapping), do: nil
1123+
1124+
defp build_many_to_many_values(list, relationship, meta_mapping) when is_list(list) do
1125+
destination_field = relationship.destination_attribute
1126+
1127+
Enum.map(list, fn
1128+
{%{id: id}, meta} ->
1129+
attrs =
1130+
Enum.reduce(meta_mapping, %{}, fn {meta_key, join_attr}, acc ->
1131+
case Map.fetch(meta, to_string(meta_key)) do
1132+
{:ok, value} -> Map.put(acc, join_attr, value)
1133+
:error -> acc
1134+
end
1135+
end)
1136+
1137+
if map_size(attrs) == 0 do
1138+
id
1139+
else
1140+
Map.put(attrs, destination_field, id)
1141+
end
1142+
1143+
%{id: id} ->
1144+
id
1145+
end)
1146+
end
1147+
1148+
defp build_many_to_many_values(%{id: id}, _relationship, _meta_mapping), do: id
10921149
end

lib/ash_json_api/resource/info.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@ defmodule AshJsonApi.Resource.Info do
7070
Extension.get_opt(resource, [:json_api], :default_fields, nil, true)
7171
end
7272

73+
@doc """
74+
Returns the `relationship_meta` config for the resource.
75+
76+
This is a keyword list keyed by relationship name, where each value is
77+
another keyword list mapping JSON:API meta keys to join resource attributes.
78+
"""
79+
def relationship_meta(resource) do
80+
Extension.get_opt(resource, [:json_api], :relationship_meta, [], true)
81+
end
82+
83+
@doc """
84+
Returns the meta-to-join-attribute mapping for the given relationship name.
85+
86+
The result is a keyword list like `[note: :note_on_join]`, where the key is the
87+
JSON:API meta key (as an atom) and the value is the join resource attribute.
88+
"""
89+
def relationship_meta_mapping(resource, relationship_name) do
90+
relationship_meta(resource)[relationship_name] || []
91+
end
92+
7393
defp camelize(name) do
7494
camelized = name |> to_string() |> Macro.camelize()
7595
{first, rest} = String.split_at(camelized, 1)

lib/ash_json_api/resource/resource.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,27 @@ defmodule AshJsonApi.Resource do
557557
"Whether or not to derive a filter parameter based on the sortable fields of the resource",
558558
default: true
559559
],
560+
relationship_meta: [
561+
type: :keyword_list,
562+
default: [],
563+
doc: """
564+
Configures how join-table fields are exposed via JSON:API `meta` on relationship
565+
resource identifiers for many_to_many relationships.
566+
567+
The value is a keyword list keyed by relationship name, where each value is
568+
another keyword list mapping JSON:API meta keys to join resource attributes.
569+
570+
For example, to expose a `:note` attribute on the join resource for the
571+
`:tags` relationship:
572+
573+
relationship_meta [
574+
tags: [note: :note]
575+
]
576+
577+
This enables both reading and writing join-table fields via the `meta` object
578+
on relationship resource identifiers for that relationship.
579+
"""
580+
],
560581
field_names: [
561582
type: {:or, [{:literal, :camelize}, {:literal, :dasherize}, :keyword_list, {:fun, 1}]},
562583
doc: """

lib/ash_json_api/serializer.ex

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@ defmodule AshJsonApi.Serializer do
1212
%{self: at_host(request, request.url)}
1313
|> add_related_link(request, source_record, relationship)
1414

15+
identifier_meta =
16+
Map.get(request.assigns, :relationship_identifier_meta, %{})
17+
1518
%{
1619
links: links,
1720
data:
18-
Enum.map(
19-
List.wrap(records),
20-
&serialize_relationship_data(&1, source_record, relationship)
21-
),
21+
Enum.map(List.wrap(records), fn record ->
22+
payload = serialize_relationship_data(record, source_record, relationship)
23+
24+
case Map.fetch(identifier_meta, AshJsonApi.Resource.encode_primary_key(record)) do
25+
{:ok, meta_for_identifier} when is_map(meta_for_identifier) and
26+
map_size(meta_for_identifier) > 0 ->
27+
Map.put(payload, :meta, meta_for_identifier)
28+
29+
_ ->
30+
payload
31+
end
32+
end),
2233
meta: meta
2334
}
2435
|> Jason.encode!()
@@ -162,6 +173,56 @@ defmodule AshJsonApi.Serializer do
162173
|> add_relationship_meta(record, source_record, relationship)
163174
end
164175

176+
defp add_relationship_meta(
177+
payload,
178+
row,
179+
source_record,
180+
%Ash.Resource.Relationships.ManyToMany{} = relationship
181+
) do
182+
source_resource = source_record.__struct__
183+
184+
meta_mapping =
185+
AshJsonApi.Resource.Info.relationship_meta_mapping(source_resource, relationship.name)
186+
187+
if meta_mapping == [] do
188+
payload
189+
else
190+
join_rows = Map.get(source_record, relationship.join_relationship)
191+
192+
if match?(%Ash.NotLoaded{}, join_rows) do
193+
payload
194+
else
195+
destination_value = Map.get(row, relationship.destination_attribute)
196+
197+
meta =
198+
join_rows
199+
|> List.wrap()
200+
|> Enum.find(fn join_row ->
201+
Map.get(join_row, relationship.destination_attribute_on_join_resource) ==
202+
destination_value
203+
end)
204+
|> case do
205+
nil ->
206+
%{}
207+
208+
join_row ->
209+
Enum.reduce(meta_mapping, %{}, fn {meta_key, join_attr}, acc ->
210+
case Map.fetch(join_row, join_attr) do
211+
{:ok, value} -> Map.put(acc, meta_key, value)
212+
:error -> acc
213+
end
214+
end)
215+
end
216+
217+
if map_size(meta) == 0 do
218+
payload
219+
else
220+
Map.put(payload, :meta, meta)
221+
end
222+
end
223+
end
224+
end
225+
165226
defp add_relationship_meta(payload, _row, _source_record, _relationship) do
166227
payload
167228
# case relationship.join_attributes do

test/acceptance/relationships_test.exs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
defmodule AshJsonApi.Acceptance.RelationshipsTest do
66
use ExUnit.Case, async: true
77

8+
require Ash.Query
9+
810
defmodule Tag do
911
use Ash.Resource,
1012
domain: AshJsonApi.Acceptance.RelationshipsTest.Domain,
@@ -55,10 +57,15 @@ defmodule AshJsonApi.Acceptance.RelationshipsTest do
5557

5658
actions do
5759
defaults([:create, :read, :destroy])
60+
61+
update :update do
62+
accept([:note])
63+
end
5864
end
5965

6066
attributes do
6167
uuid_primary_key(:id)
68+
attribute(:note, :string)
6269
end
6370

6471
relationships do
@@ -80,6 +87,10 @@ defmodule AshJsonApi.Acceptance.RelationshipsTest do
8087
json_api do
8188
type("person")
8289

90+
relationship_meta [
91+
tags: [note: :note]
92+
]
93+
8394
routes do
8495
base("/people")
8596
relationship :tags, :read
@@ -202,22 +213,28 @@ defmodule AshJsonApi.Acceptance.RelationshipsTest do
202213
]
203214
}
204215

205-
@domain
206-
|> post(
207-
"/people/#{person.id}/relationships/tags",
208-
body,
209-
router: @router,
210-
status: 200
211-
)
212-
|> assert_valid_resource_objects("tag", [tag.id])
213-
214-
@domain
215-
|> get(
216-
"/people/#{person.id}/relationships/tags",
217-
router: @router,
218-
status: 200
219-
)
220-
|> assert_valid_resource_objects("tag", [tag.id])
216+
response =
217+
@domain
218+
|> post(
219+
"/people/#{person.id}/relationships/tags",
220+
body,
221+
router: @router,
222+
status: 200
223+
)
224+
225+
assert_valid_resource_objects(response, "tag", [tag.id])
226+
227+
assert %{
228+
"data" => [
229+
%{
230+
"id" => id,
231+
"type" => "tag",
232+
"meta" => %{"note" => "any"}
233+
}
234+
]
235+
} = response.resp_body
236+
237+
assert id == tag.id
221238
end
222239

223240
test "post_to_relationship accepts multiple identifiers" do

0 commit comments

Comments
 (0)