Skip to content

Commit a2ed3ec

Browse files
authored
Fix JSON schema field visibility (#441)
1 parent 39fce74 commit a2ed3ec

2 files changed

Lines changed: 187 additions & 1 deletion

File tree

lib/ash_json_api/json_schema/json_schema.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule AshJsonApi.JsonSchema do
2525
{new_refs, route_schemas} =
2626
resource
2727
|> AshJsonApi.Resource.Info.routes(domains)
28+
|> Enum.filter(&route_visible?(resource, &1))
2829
|> Enum.reduce({[], []}, fn route, {refs, route_schemas} ->
2930
{new_refs, new_route_schema} =
3031
route_schema(route, domain, resource, opts)
@@ -278,6 +279,7 @@ defmodule AshJsonApi.JsonSchema do
278279
defp required_attributes(resource) do
279280
resource
280281
|> Ash.Resource.Info.public_attributes()
282+
|> filter_shown_fields(resource)
281283
|> Enum.reject(&(&1.allow_nil? || AshJsonApi.Resource.only_primary_key?(resource, &1.name)))
282284
|> Enum.map(&AshJsonApi.Resource.Info.field_to_json_key(resource, &1.name))
283285
end
@@ -290,6 +292,7 @@ defmodule AshJsonApi.JsonSchema do
290292
Ash.Resource.Info.public_aggregates(resource)
291293
|> set_aggregate_constraints(resource)
292294
)
295+
|> filter_shown_fields(resource)
293296
|> Enum.reject(&AshJsonApi.Resource.only_primary_key?(resource, &1.name))
294297
|> Enum.reduce(%{}, fn attr, acc ->
295298
Map.put(
@@ -332,6 +335,7 @@ defmodule AshJsonApi.JsonSchema do
332335
defp resource_relationships(resource) do
333336
resource
334337
|> Ash.Resource.Info.public_relationships()
338+
|> filter_shown_fields(resource)
335339
|> Enum.filter(fn relationship ->
336340
AshJsonApi.Resource.Info.type(relationship.destination)
337341
end)
@@ -719,6 +723,24 @@ defmodule AshJsonApi.JsonSchema do
719723
action && action.type == :read
720724
end
721725

726+
defp show_field?(resource, %{name: name}) do
727+
AshJsonApi.Resource.Info.show_field?(resource, name)
728+
end
729+
730+
defp show_field?(resource, field) do
731+
AshJsonApi.Resource.Info.show_field?(resource, field)
732+
end
733+
734+
defp filter_shown_fields(fields, resource) do
735+
Enum.filter(fields, &show_field?(resource, &1))
736+
end
737+
738+
defp route_visible?(_resource, %{relationship: nil}), do: true
739+
740+
defp route_visible?(resource, %{relationship: relationship}) do
741+
show_field?(resource, relationship)
742+
end
743+
722744
defp add_route_properties(keys, resource, properties) do
723745
Enum.reduce(properties, keys, fn property, keys ->
724746
spec =
@@ -760,7 +782,7 @@ defmodule AshJsonApi.JsonSchema do
760782
end
761783

762784
defp sort_format(resource) do
763-
sorts = sortable_fields(resource)
785+
sorts = resource |> sortable_fields() |> filter_shown_fields(resource)
764786

765787
"(#{Enum.map_join(sorts, "|", &AshJsonApi.Resource.Info.field_to_json_key(resource, &1.name))}),*"
766788
end
@@ -985,6 +1007,7 @@ defmodule AshJsonApi.JsonSchema do
9851007
resource
9861008
|> Ash.Resource.Info.attributes()
9871009
|> Enum.filter(&(&1.name in action.accept && &1.writable?))
1010+
|> filter_shown_fields(resource)
9881011
|> Enum.reduce(%{}, fn attribute, acc ->
9891012
Map.put(
9901013
acc,
@@ -1023,13 +1046,15 @@ defmodule AshJsonApi.JsonSchema do
10231046
defp required_relationship_attributes(resource, relationship_arguments, action) do
10241047
action.arguments
10251048
|> Enum.filter(&has_relationship_argument?(relationship_arguments, &1.name))
1049+
|> filter_shown_fields(resource)
10261050
|> Enum.reject(& &1.allow_nil?)
10271051
|> Enum.map(&AshJsonApi.Resource.Info.argument_to_json_key(resource, action.name, &1.name))
10281052
end
10291053

10301054
defp write_relationships(resource, relationship_arguments, action) do
10311055
action.arguments
10321056
|> Enum.filter(&has_relationship_argument?(relationship_arguments, &1.name))
1057+
|> filter_shown_fields(resource)
10331058
|> Enum.reduce(%{}, fn argument, acc ->
10341059
data = resource_relationship_field_data(resource, argument)
10351060

test/acceptance/json_schema_test.exs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,120 @@ defmodule Test.Acceptance.JsonSchemaTest do
151151
end
152152
end
153153

154+
defmodule HiddenSpecAuthor do
155+
use Ash.Resource,
156+
domain: Test.Acceptance.JsonSchemaTest.HiddenSpecDomain,
157+
data_layer: Ash.DataLayer.Ets,
158+
extensions: [
159+
AshJsonApi.Resource
160+
]
161+
162+
ets do
163+
private?(true)
164+
end
165+
166+
json_api do
167+
type("hidden-json-author")
168+
169+
routes do
170+
base("/hidden_json_authors")
171+
index(:read)
172+
end
173+
end
174+
175+
actions do
176+
default_accept(:*)
177+
defaults([:read, :create])
178+
end
179+
180+
attributes do
181+
uuid_primary_key(:id, writable?: true, public?: true)
182+
attribute(:name, :string, allow_nil?: false, public?: true)
183+
end
184+
end
185+
186+
defmodule HiddenSpecPost do
187+
use Ash.Resource,
188+
domain: Test.Acceptance.JsonSchemaTest.HiddenSpecDomain,
189+
data_layer: Ash.DataLayer.Ets,
190+
extensions: [
191+
AshJsonApi.Resource
192+
]
193+
194+
ets do
195+
private?(true)
196+
end
197+
198+
json_api do
199+
type("hidden-json-post")
200+
default_fields([:name, :secret, :secret_calc])
201+
hide_fields([:secret, :secret_calc, :hidden_author])
202+
203+
routes do
204+
base("/hidden_json_posts")
205+
get(:read)
206+
index(:read)
207+
post(:create, relationship_arguments: [{:id, :visible_author}, {:id, :hidden_author}])
208+
related(:visible_author, :read)
209+
related(:hidden_author, :read)
210+
end
211+
end
212+
213+
actions do
214+
default_accept(:*)
215+
defaults([:read])
216+
217+
create :create do
218+
primary? true
219+
accept([:id, :name, :secret])
220+
argument(:visible_author, :uuid, allow_nil?: false)
221+
argument(:hidden_author, :uuid, allow_nil?: false)
222+
223+
change(manage_relationship(:visible_author, type: :append_and_remove))
224+
change(manage_relationship(:hidden_author, type: :append_and_remove))
225+
end
226+
end
227+
228+
attributes do
229+
uuid_primary_key(:id, writable?: true, public?: true)
230+
attribute(:name, :string, allow_nil?: false, public?: true)
231+
attribute(:secret, :string, allow_nil?: false, public?: true)
232+
end
233+
234+
calculations do
235+
calculate(:secret_calc, :string, concat([:name, :name], "-"), public?: true)
236+
end
237+
238+
relationships do
239+
belongs_to(:visible_author, Test.Acceptance.JsonSchemaTest.HiddenSpecAuthor,
240+
allow_nil?: false,
241+
public?: true
242+
)
243+
244+
belongs_to(:hidden_author, Test.Acceptance.JsonSchemaTest.HiddenSpecAuthor,
245+
allow_nil?: false,
246+
public?: true
247+
)
248+
end
249+
end
250+
251+
defmodule HiddenSpecDomain do
252+
use Ash.Domain,
253+
otp_app: :ash_json_api,
254+
extensions: [
255+
AshJsonApi.Domain
256+
]
257+
258+
json_api do
259+
log_errors?(false)
260+
end
261+
262+
resources do
263+
resource(HiddenSpecAuthor)
264+
resource(HiddenSpecPost)
265+
end
266+
end
267+
154268
defmodule Blogs do
155269
use Ash.Domain,
156270
otp_app: :ash_json_api,
@@ -189,6 +303,53 @@ defmodule Test.Acceptance.JsonSchemaTest do
189303
)
190304
end
191305

306+
test "hide_fields hides fields from the generated JSON schema" do
307+
json_api = AshJsonApi.JsonSchema.generate([HiddenSpecDomain])
308+
309+
post_schema = json_api["definitions"]["hidden-json-post"]
310+
attributes = post_schema["properties"]["attributes"]["properties"]
311+
relationships = post_schema["properties"]["relationships"]["properties"]
312+
313+
assert Map.has_key?(attributes, "name")
314+
refute Map.has_key?(attributes, "secret")
315+
refute Map.has_key?(attributes, "secret_calc")
316+
refute "secret" in post_schema["properties"]["attributes"]["required"]
317+
318+
assert Map.has_key?(relationships, "visible_author")
319+
refute Map.has_key?(relationships, "hidden_author")
320+
321+
hrefs = Enum.map(json_api["links"], & &1["href"])
322+
assert Enum.any?(hrefs, &String.contains?(&1, "/hidden_json_posts/{id}/visible_author"))
323+
refute Enum.any?(hrefs, &String.contains?(&1, "/hidden_json_posts/{id}/hidden_author"))
324+
325+
index_link =
326+
Enum.find(json_api["links"], fn link ->
327+
link["rel"] == "index" && String.starts_with?(link["href"], "/hidden_json_posts")
328+
end)
329+
330+
assert index_link["hrefSchema"]["properties"]["sort"]["format"] =~ "name"
331+
refute index_link["hrefSchema"]["properties"]["sort"]["format"] =~ "secret"
332+
333+
create_link =
334+
Enum.find(json_api["links"], fn link ->
335+
link["rel"] == "post" && String.starts_with?(link["href"], "/hidden_json_posts")
336+
end)
337+
338+
create_attributes = create_link["schema"]["properties"]["data"]["properties"]["attributes"]
339+
340+
create_relationships =
341+
create_link["schema"]["properties"]["data"]["properties"]["relationships"]
342+
343+
assert Map.has_key?(create_attributes["properties"], "name")
344+
refute Map.has_key?(create_attributes["properties"], "secret")
345+
refute "secret" in create_attributes["required"]
346+
347+
assert Map.has_key?(create_relationships["properties"], "visible_author")
348+
refute Map.has_key?(create_relationships["properties"], "hidden_author")
349+
assert "visible_author" in create_relationships["required"]
350+
refute "hidden_author" in create_relationships["required"]
351+
end
352+
192353
test "handles self-referential embedded resources without infinite loop" do
193354
# This should complete without timing out
194355
# If it loops infinitely, the test will timeout

0 commit comments

Comments
 (0)