Skip to content

Commit 6e5fa4f

Browse files
authored
fix: remove '0' bulk index in error source pointers (#430)
1 parent 2d39c25 commit 6e5fa4f

2 files changed

Lines changed: 93 additions & 2 deletions

File tree

lib/ash_json_api/controllers/helpers.ex

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ defmodule AshJsonApi.Controllers.Helpers do
448448
Request.add_error(request, error, :fetch_from_path)
449449

450450
%Ash.BulkResult{status: :error, errors: errors} ->
451-
Request.add_error(request, errors, :update)
451+
Request.add_error(request, strip_bulk_index_from_errors(errors), :update)
452452
end
453453
end
454454
end
@@ -635,7 +635,7 @@ defmodule AshJsonApi.Controllers.Helpers do
635635
Request.add_error(request, error, :fetch_from_path)
636636

637637
%Ash.BulkResult{status: :error, errors: errors} ->
638-
Request.add_error(request, errors, :update)
638+
Request.add_error(request, strip_bulk_index_from_errors(errors), :destroy)
639639
end
640640
end
641641
end
@@ -1165,4 +1165,34 @@ defmodule AshJsonApi.Controllers.Helpers do
11651165
{:ok, updated}
11661166
end
11671167
end
1168+
1169+
# Strips the bulk operation index (0) from error paths.
1170+
# When using Ash.bulk_update/bulk_destroy for single-record operations,
1171+
# errors include a leading 0 index that should not appear in JSON:API responses.
1172+
defp strip_bulk_index_from_errors(errors) do
1173+
Enum.map(errors, &strip_bulk_index_from_single_error/1)
1174+
end
1175+
1176+
defp strip_bulk_index_from_single_error(error) do
1177+
error
1178+
|> strip_path_from_error()
1179+
|> strip_path_from_inner_errors()
1180+
end
1181+
1182+
defp strip_path_from_error(error) do
1183+
case Map.get(error, :path) do
1184+
[0 | rest] -> %{error | path: rest}
1185+
_ -> error
1186+
end
1187+
end
1188+
1189+
defp strip_path_from_inner_errors(error) do
1190+
case Map.get(error, :errors) do
1191+
errors when is_list(errors) and errors != [] ->
1192+
%{error | errors: Enum.map(errors, &strip_bulk_index_from_single_error/1)}
1193+
1194+
_ ->
1195+
error
1196+
end
1197+
end
11681198
end

test/acceptance/patch_test.exs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ defmodule Test.Acceptance.PatchTest do
208208
route "/private_arg_update/:id"
209209
end
210210

211+
patch :validated_update do
212+
route "/validated_update/:id"
213+
end
214+
211215
related :author, :read
212216
patch_relationship :author
213217
end
@@ -253,6 +257,23 @@ defmodule Test.Acceptance.PatchTest do
253257
end
254258
end
255259

260+
update :validated_update do
261+
accept([:name])
262+
require_atomic?(false)
263+
264+
validate(fn changeset, _context ->
265+
if Ash.Changeset.changing_attribute?(changeset, :name) do
266+
{:error,
267+
Ash.Error.Changes.InvalidAttribute.exception(
268+
field: :name,
269+
message: "cannot be changed"
270+
)}
271+
else
272+
:ok
273+
end
274+
end)
275+
end
276+
256277
action :forbidden_update, :struct do
257278
constraints(instance_of: __MODULE__)
258279
argument(:id, :uuid, allow_nil?: false)
@@ -885,4 +906,44 @@ defmodule Test.Acceptance.PatchTest do
885906
assert bio_content == bio.bio
886907
end
887908
end
909+
910+
describe "single-record error source pointers" do
911+
setup do
912+
post =
913+
Post
914+
|> Ash.Changeset.for_create(:create, %{id: Ecto.UUID.generate(), name: "Test Post"})
915+
|> Ash.create!()
916+
917+
%{post: post}
918+
end
919+
920+
test "validation errors do not include bulk index in source pointer", %{post: post} do
921+
# This test verifies the fix for the bug where single-record operations
922+
# would include a `/0/` bulk index in error source pointers.
923+
# Before the fix: source pointer was "/data/attributes/0/name"
924+
# After the fix: source pointer is "/data/attributes/name"
925+
response =
926+
Domain
927+
|> patch(
928+
"/posts/validated_update/#{post.id}",
929+
%{
930+
data: %{
931+
type: "post",
932+
attributes: %{
933+
name: "new_name"
934+
}
935+
}
936+
},
937+
status: 400
938+
)
939+
940+
assert %{"errors" => [error]} = response.resp_body
941+
assert error["code"] == "invalid_attribute"
942+
943+
# The source pointer should NOT contain "/0/" - that's the bulk index
944+
# which should be filtered out for single-record operations
945+
assert error["source"]["pointer"] == "/data/attributes/name"
946+
refute error["source"]["pointer"] =~ ~r"/\d+/"
947+
end
948+
end
888949
end

0 commit comments

Comments
 (0)