Skip to content

Commit 2a442e2

Browse files
committed
nested record duplicates
1 parent 71dc1f0 commit 2a442e2

File tree

2 files changed

+292
-0
lines changed

2 files changed

+292
-0
lines changed

lib/ash_json_api/includes/includer.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ defmodule AshJsonApi.Includes.Includer do
4545
|> Map.get(relationship, [])
4646
|> List.wrap()
4747
end)
48+
|> Enum.uniq()
4849
|> get_includes_map(further, includes_map)
4950

5051
preloaded_with_linkage =
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
defmodule AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources do
2+
use ExUnit.Case
3+
@moduletag :json_api_spec_1_0
4+
5+
defmodule Author do
6+
use Ash.Resource,
7+
domain: AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Domain,
8+
data_layer: Ash.DataLayer.Ets,
9+
extensions: [AshJsonApi.Resource]
10+
11+
ets do
12+
private?(true)
13+
end
14+
15+
json_api do
16+
type("author")
17+
18+
routes do
19+
base("/authors")
20+
get(:read)
21+
index(:read)
22+
end
23+
24+
includes posts: [image: [file: []]]
25+
end
26+
27+
attributes do
28+
uuid_primary_key(:id)
29+
attribute(:name, :string, public?: true)
30+
end
31+
32+
actions do
33+
default_accept(:*)
34+
defaults([:create, :read, :update, :destroy])
35+
end
36+
37+
relationships do
38+
has_many(:posts, AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Post,
39+
public?: true,
40+
destination_attribute: :author_id
41+
)
42+
end
43+
end
44+
45+
defmodule Image do
46+
use Ash.Resource,
47+
domain: AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Domain,
48+
data_layer: Ash.DataLayer.Ets,
49+
extensions: [AshJsonApi.Resource]
50+
51+
ets do
52+
private?(true)
53+
end
54+
55+
json_api do
56+
type("image")
57+
58+
routes do
59+
base("/images")
60+
get(:read)
61+
index(:read)
62+
end
63+
end
64+
65+
attributes do
66+
uuid_primary_key(:id)
67+
attribute(:name, :string, public?: true)
68+
end
69+
70+
actions do
71+
default_accept(:*)
72+
defaults([:create, :read, :update, :destroy])
73+
end
74+
75+
relationships do
76+
belongs_to(:file, AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.File,
77+
public?: true
78+
)
79+
80+
has_many(:posts, AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Post,
81+
public?: true,
82+
destination_attribute: :image_id
83+
)
84+
end
85+
end
86+
87+
defmodule File do
88+
use Ash.Resource,
89+
domain: AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Domain,
90+
data_layer: Ash.DataLayer.Ets,
91+
extensions: [AshJsonApi.Resource]
92+
93+
ets do
94+
private?(true)
95+
end
96+
97+
json_api do
98+
type("file")
99+
100+
routes do
101+
base("/files")
102+
get(:read)
103+
index(:read)
104+
end
105+
end
106+
107+
attributes do
108+
uuid_primary_key(:id)
109+
attribute(:name, :string, public?: true)
110+
end
111+
112+
actions do
113+
default_accept(:*)
114+
defaults([:create, :read, :update, :destroy])
115+
end
116+
117+
relationships do
118+
has_many(:images, AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Image,
119+
public?: true,
120+
destination_attribute: :file_id
121+
)
122+
end
123+
end
124+
125+
defmodule Post do
126+
use Ash.Resource,
127+
domain: AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Domain,
128+
data_layer: Ash.DataLayer.Ets,
129+
extensions: [AshJsonApi.Resource]
130+
131+
actions do
132+
default_accept(:*)
133+
defaults([:create, :read, :update, :destroy])
134+
end
135+
136+
ets do
137+
private?(true)
138+
end
139+
140+
json_api do
141+
type("post")
142+
143+
routes do
144+
base("/posts")
145+
get(:read)
146+
index(:read)
147+
end
148+
end
149+
150+
attributes do
151+
uuid_primary_key(:id)
152+
attribute(:name, :string, public?: true)
153+
end
154+
155+
relationships do
156+
belongs_to(:author, Author, public?: true)
157+
belongs_to(:image, Image, public?: true)
158+
159+
has_many(:comments, AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Comment,
160+
public?: true
161+
)
162+
end
163+
end
164+
165+
defmodule Comment do
166+
use Ash.Resource,
167+
domain: AshJsonApiTest.FetchingData.InclusionOfNestedRelatedResources.Domain,
168+
data_layer: Ash.DataLayer.Ets,
169+
extensions: [AshJsonApi.Resource]
170+
171+
actions do
172+
default_accept(:*)
173+
defaults([:create, :read, :update, :destroy])
174+
end
175+
176+
ets do
177+
private?(true)
178+
end
179+
180+
json_api do
181+
type("comment")
182+
default_fields [:text, :calc]
183+
end
184+
185+
attributes do
186+
uuid_primary_key(:id)
187+
attribute(:text, :string, public?: true)
188+
end
189+
190+
calculations do
191+
calculate(:calc, :string, expr("hello"))
192+
end
193+
194+
relationships do
195+
belongs_to(:post, Post, public?: true)
196+
end
197+
end
198+
199+
defmodule Domain do
200+
use Ash.Domain,
201+
otp_app: :ash_json_api,
202+
extensions: [
203+
AshJsonApi.Domain
204+
]
205+
206+
resources do
207+
resource(Author)
208+
resource(Image)
209+
resource(File)
210+
resource(Post)
211+
resource(Comment)
212+
end
213+
end
214+
215+
defmodule Router do
216+
use AshJsonApi.Router, domain: Domain
217+
end
218+
219+
import AshJsonApi.Test
220+
221+
setup do
222+
Application.put_env(:ash_json_api, Domain, json_api: [test_router: Router])
223+
224+
:ok
225+
end
226+
227+
# credo:disable-for-this-file Credo.Check.Readability.MaxLineLength
228+
229+
# JSON:API 1.0 Specification
230+
# --------------------------
231+
# An endpoint MAY also support an include request parameter to allow the client to customize which related resources should be returned.
232+
# --------------------------
233+
describe "include request parameter with nested relations" do
234+
@describetag :spec_may
235+
236+
test "resources endpoint with included param of to-many.to-one.to-one relationship" do
237+
# GET /authors/?include=posts.image.file
238+
239+
file =
240+
%{id: file_id} =
241+
File
242+
|> Ash.Changeset.for_create(:create, %{name: "foo"})
243+
|> Ash.create!()
244+
245+
image =
246+
Image
247+
|> Ash.Changeset.for_create(:create, %{name: "foo"})
248+
|> Ash.Changeset.manage_relationship(:file, file, type: :append_and_remove)
249+
|> Ash.create!()
250+
251+
author =
252+
Author
253+
|> Ash.Changeset.for_create(:create, %{name: "foo"})
254+
|> Ash.create!()
255+
256+
_post_1 =
257+
Post
258+
|> Ash.Changeset.for_create(:create, %{name: "foo"})
259+
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
260+
|> Ash.Changeset.manage_relationship(:image, image, type: :append_and_remove)
261+
|> Ash.create!()
262+
263+
_post_2 =
264+
Post
265+
|> Ash.Changeset.for_create(:create, %{name: "bar"})
266+
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
267+
|> Ash.Changeset.manage_relationship(:image, image, type: :append_and_remove)
268+
|> Ash.create!()
269+
270+
assert %{
271+
resp_body: %{
272+
"data" => [_ | _],
273+
"included" => included
274+
}
275+
} = get(Domain, "/authors/?include=posts.image.file", status: 200)
276+
277+
assert included_image = Enum.find(included, &(&1["id"] == image.id))
278+
279+
assert %{
280+
"relationships" => %{
281+
"file" => %{
282+
"data" => %{
283+
"id" => ^file_id,
284+
"type" => "file"
285+
}
286+
}
287+
}
288+
} = included_image
289+
end
290+
end
291+
end

0 commit comments

Comments
 (0)