Skip to content

Commit 0436843

Browse files
authored
improvement: support mapping join resource keys to meta (#419)
1 parent 081a217 commit 0436843

5 files changed

Lines changed: 222 additions & 26 deletions

File tree

lib/ash_json_api/controllers/helpers.ex

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ 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+
relationship = Ash.Resource.Info.relationship(request.resource, relationship_name)
462+
values = relationship_change_values(request, relationship_name)
462463

463464
result
464465
|> Ash.Changeset.new()
@@ -469,8 +470,15 @@ defmodule AshJsonApi.Controllers.Helpers do
469470
|> Ash.update(Request.opts(request))
470471
|> case do
471472
{:ok, updated} ->
472-
request
473-
|> Request.assign(:result, Map.get(updated, relationship_name))
473+
case load_result_for_relationship_meta(updated, relationship, request) do
474+
{:ok, updated_with_join_rows} ->
475+
request
476+
|> Request.assign(:result, Map.get(updated_with_join_rows, relationship_name))
477+
|> Request.assign(:record_from_path, updated_with_join_rows)
478+
479+
{:error, error} ->
480+
Request.add_error(request, error, :add_to_relationship)
481+
end
474482

475483
{:error, error} ->
476484
Request.add_error(request, error, :add_to_relationship)
@@ -481,7 +489,8 @@ defmodule AshJsonApi.Controllers.Helpers do
481489
def replace_relationship(request, relationship_name) do
482490
chain(request, fn %{assigns: %{result: result}} ->
483491
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
484-
values = normalize_relationship_identifiers(request)
492+
relationship = Ash.Resource.Info.relationship(request.resource, relationship_name)
493+
values = relationship_change_values(request, relationship_name)
485494

486495
result
487496
|> Ash.Changeset.new()
@@ -492,8 +501,15 @@ defmodule AshJsonApi.Controllers.Helpers do
492501
|> Ash.update(Request.opts(request))
493502
|> case do
494503
{:ok, updated} ->
495-
request
496-
|> Request.assign(:result, Map.get(updated, relationship_name))
504+
case load_result_for_relationship_meta(updated, relationship, request) do
505+
{:ok, updated_with_join_rows} ->
506+
request
507+
|> Request.assign(:result, Map.get(updated_with_join_rows, relationship_name))
508+
|> Request.assign(:record_from_path, updated_with_join_rows)
509+
510+
{:error, error} ->
511+
Request.add_error(request, error, :replace_relationship)
512+
end
497513

498514
{:error, error} ->
499515
Request.add_error(request, error, :replace_relationship)
@@ -504,7 +520,8 @@ 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+
relationship = Ash.Resource.Info.relationship(request.resource, relationship_name)
524+
values = relationship_change_values(request, relationship_name)
508525

509526
result
510527
|> Ash.Changeset.new()
@@ -518,8 +535,15 @@ defmodule AshJsonApi.Controllers.Helpers do
518535
|> Ash.load(fields(request, request.resource), Request.load_opts(request))
519536
|> case do
520537
{:ok, updated} ->
521-
request
522-
|> Request.assign(:result, Map.get(updated, relationship_name))
538+
case load_result_for_relationship_meta(updated, relationship, request) do
539+
{:ok, updated_with_join_rows} ->
540+
request
541+
|> Request.assign(:result, Map.get(updated_with_join_rows, relationship_name))
542+
|> Request.assign(:record_from_path, updated_with_join_rows)
543+
544+
{:error, error} ->
545+
Request.add_error(request, error, :delete_from_relationship)
546+
end
523547

524548
{:error, error} ->
525549
Request.add_error(request, error, :delete_from_relationship)
@@ -1089,4 +1113,55 @@ defmodule AshJsonApi.Controllers.Helpers do
10891113
id
10901114
end
10911115
end
1116+
1117+
defp relationship_change_values(request, relationship_name) do
1118+
relationship = Ash.Resource.Info.relationship(request.resource, relationship_name)
1119+
1120+
meta_mapping =
1121+
AshJsonApi.Resource.Info.relationship_meta_in_mapping(request.resource, relationship_name)
1122+
1123+
if match?(%Ash.Resource.Relationships.ManyToMany{}, relationship) and meta_mapping != [] do
1124+
build_many_to_many_values(request.resource_identifiers, relationship, meta_mapping)
1125+
else
1126+
normalize_relationship_identifiers(request)
1127+
end
1128+
end
1129+
1130+
defp build_many_to_many_values(nil, _relationship, _meta_mapping), do: nil
1131+
1132+
defp build_many_to_many_values(list, relationship, meta_mapping) when is_list(list) do
1133+
destination_field = relationship.destination_attribute
1134+
1135+
Enum.map(list, fn
1136+
{%{id: id}, meta} ->
1137+
attrs =
1138+
Enum.reduce(meta_mapping, %{}, fn {meta_key, join_attr}, acc ->
1139+
case Map.fetch(meta, to_string(meta_key)) do
1140+
{:ok, value} -> Map.put(acc, join_attr, value)
1141+
:error -> acc
1142+
end
1143+
end)
1144+
1145+
if map_size(attrs) == 0 do
1146+
id
1147+
else
1148+
Map.put(attrs, destination_field, id)
1149+
end
1150+
1151+
%{id: id} ->
1152+
id
1153+
end)
1154+
end
1155+
1156+
defp build_many_to_many_values(%{id: id}, _relationship, _meta_mapping), do: id
1157+
1158+
defp load_result_for_relationship_meta(updated, relationship, request) do
1159+
case relationship do
1160+
%Ash.Resource.Relationships.ManyToMany{join_relationship: join_relationship} ->
1161+
Ash.load(updated, [join_relationship], Request.load_opts(request))
1162+
1163+
_ ->
1164+
{:ok, updated}
1165+
end
1166+
end
10921167
end

lib/ash_json_api/resource/info.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,34 @@ 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_in` config for the resource.
75+
"""
76+
def relationship_meta_in(resource) do
77+
Extension.get_opt(resource, [:json_api], :relationship_meta_in, [], true)
78+
end
79+
80+
@doc """
81+
Returns the `relationship_meta_out` config for the resource.
82+
"""
83+
def relationship_meta_out(resource) do
84+
Extension.get_opt(resource, [:json_api], :relationship_meta_out, [], true)
85+
end
86+
87+
@doc """
88+
Returns the incoming meta-to-join-attribute mapping for the given relationship name.
89+
"""
90+
def relationship_meta_in_mapping(resource, relationship_name) do
91+
relationship_meta_in(resource)[relationship_name] || []
92+
end
93+
94+
@doc """
95+
Returns the outgoing meta-to-join-attribute mapping for the given relationship name.
96+
"""
97+
def relationship_meta_out_mapping(resource, relationship_name) do
98+
relationship_meta_out(resource)[relationship_name] || []
99+
end
100+
73101
defp camelize(name) do
74102
camelized = name |> to_string() |> Macro.camelize()
75103
{first, rest} = String.split_at(camelized, 1)

lib/ash_json_api/resource/resource.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,28 @@ 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_in: [
561+
type: :keyword_list,
562+
default: [],
563+
doc: """
564+
Configures how incoming JSON:API `meta` keys on relationship resource identifiers
565+
map to join resource attributes for many_to_many relationship writes.
566+
567+
Use together with `relationship_meta_out` for reads. Each relationship you want to
568+
support must declare both mappings explicitly.
569+
"""
570+
],
571+
relationship_meta_out: [
572+
type: :keyword_list,
573+
default: [],
574+
doc: """
575+
Configures how join resource attributes map to outgoing JSON:API `meta` keys on
576+
relationship resource identifiers for many_to_many relationship reads.
577+
578+
Use together with `relationship_meta_in` for writes. Each relationship you want to
579+
support must declare both mappings explicitly.
580+
"""
581+
],
560582
field_names: [
561583
type: {:or, [{:literal, :camelize}, {:literal, :dasherize}, :keyword_list, {:fun, 1}]},
562584
doc: """

lib/ash_json_api/serializer.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,56 @@ defmodule AshJsonApi.Serializer do
162162
|> add_relationship_meta(record, source_record, relationship)
163163
end
164164

165+
defp add_relationship_meta(
166+
payload,
167+
row,
168+
source_record,
169+
%Ash.Resource.Relationships.ManyToMany{} = relationship
170+
) do
171+
source_resource = source_record.__struct__
172+
173+
meta_mapping =
174+
AshJsonApi.Resource.Info.relationship_meta_out_mapping(source_resource, relationship.name)
175+
176+
if meta_mapping == [] do
177+
payload
178+
else
179+
join_rows = Map.get(source_record, relationship.join_relationship)
180+
181+
if match?(%Ash.NotLoaded{}, join_rows) do
182+
payload
183+
else
184+
destination_value = Map.get(row, relationship.destination_attribute)
185+
186+
meta =
187+
join_rows
188+
|> List.wrap()
189+
|> Enum.find(fn join_row ->
190+
Map.get(join_row, relationship.destination_attribute_on_join_resource) ==
191+
destination_value
192+
end)
193+
|> case do
194+
nil ->
195+
%{}
196+
197+
join_row ->
198+
Enum.reduce(meta_mapping, %{}, fn {meta_key, join_attr}, acc ->
199+
case Map.fetch(join_row, join_attr) do
200+
{:ok, value} -> Map.put(acc, meta_key, value)
201+
:error -> acc
202+
end
203+
end)
204+
end
205+
206+
if map_size(meta) == 0 do
207+
payload
208+
else
209+
Map.put(payload, :meta, meta)
210+
end
211+
end
212+
end
213+
end
214+
165215
defp add_relationship_meta(payload, _row, _source_record, _relationship) do
166216
payload
167217
# case relationship.join_attributes do

test/acceptance/relationships_test.exs

Lines changed: 38 additions & 17 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_in(tags: [note: :note])
91+
92+
relationship_meta_out(tags: [note_out: :note])
93+
8394
routes do
8495
base("/people")
8596
relationship :tags, :read
@@ -198,26 +209,36 @@ defmodule AshJsonApi.Acceptance.RelationshipsTest do
198209

199210
body = %{
200211
"data" => [
201-
%{"type" => "tag", "id" => tag.id, "meta" => %{"note" => "any"}}
212+
%{"type" => "tag", "id" => tag.id, "meta" => %{"note" => "any", "ignored" => "value"}}
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" => meta
233+
}
234+
]
235+
} = response.resp_body
236+
237+
assert id == tag.id
238+
assert Map.has_key?(meta, "note_out")
239+
refute Map.get(meta, "note_out") == "any"
240+
refute Map.has_key?(meta, "ignored")
241+
refute Map.has_key?(meta, "note")
221242
end
222243

223244
test "post_to_relationship accepts multiple identifiers" do

0 commit comments

Comments
 (0)