Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion lib/ash_json_api/json_schema/json_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ defmodule AshJsonApi.JsonSchema do
{new_refs, route_schemas} =
resource
|> AshJsonApi.Resource.Info.routes(domains)
|> Enum.filter(&route_visible?(resource, &1))
|> Enum.reduce({[], []}, fn route, {refs, route_schemas} ->
{new_refs, new_route_schema} =
route_schema(route, domain, resource, opts)
Expand Down Expand Up @@ -278,6 +279,7 @@ defmodule AshJsonApi.JsonSchema do
defp required_attributes(resource) do
resource
|> Ash.Resource.Info.public_attributes()
|> filter_shown_fields(resource)
|> Enum.reject(&(&1.allow_nil? || AshJsonApi.Resource.only_primary_key?(resource, &1.name)))
|> Enum.map(&AshJsonApi.Resource.Info.field_to_json_key(resource, &1.name))
end
Expand All @@ -290,6 +292,7 @@ defmodule AshJsonApi.JsonSchema do
Ash.Resource.Info.public_aggregates(resource)
|> set_aggregate_constraints(resource)
)
|> filter_shown_fields(resource)
|> Enum.reject(&AshJsonApi.Resource.only_primary_key?(resource, &1.name))
|> Enum.reduce(%{}, fn attr, acc ->
Map.put(
Expand Down Expand Up @@ -332,6 +335,7 @@ defmodule AshJsonApi.JsonSchema do
defp resource_relationships(resource) do
resource
|> Ash.Resource.Info.public_relationships()
|> filter_shown_fields(resource)
|> Enum.filter(fn relationship ->
AshJsonApi.Resource.Info.type(relationship.destination)
end)
Expand Down Expand Up @@ -719,6 +723,24 @@ defmodule AshJsonApi.JsonSchema do
action && action.type == :read
end

defp show_field?(resource, %{name: name}) do
AshJsonApi.Resource.Info.show_field?(resource, name)
end

defp show_field?(resource, field) do
AshJsonApi.Resource.Info.show_field?(resource, field)
end

defp filter_shown_fields(fields, resource) do
Enum.filter(fields, &show_field?(resource, &1))
end

defp route_visible?(_resource, %{relationship: nil}), do: true

defp route_visible?(resource, %{relationship: relationship}) do
show_field?(resource, relationship)
end

defp add_route_properties(keys, resource, properties) do
Enum.reduce(properties, keys, fn property, keys ->
spec =
Expand Down Expand Up @@ -760,7 +782,7 @@ defmodule AshJsonApi.JsonSchema do
end

defp sort_format(resource) do
sorts = sortable_fields(resource)
sorts = resource |> sortable_fields() |> filter_shown_fields(resource)

"(#{Enum.map_join(sorts, "|", &AshJsonApi.Resource.Info.field_to_json_key(resource, &1.name))}),*"
end
Expand Down Expand Up @@ -985,6 +1007,7 @@ defmodule AshJsonApi.JsonSchema do
resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(&(&1.name in action.accept && &1.writable?))
|> filter_shown_fields(resource)
|> Enum.reduce(%{}, fn attribute, acc ->
Map.put(
acc,
Expand Down Expand Up @@ -1023,13 +1046,15 @@ defmodule AshJsonApi.JsonSchema do
defp required_relationship_attributes(resource, relationship_arguments, action) do
action.arguments
|> Enum.filter(&has_relationship_argument?(relationship_arguments, &1.name))
|> filter_shown_fields(resource)
|> Enum.reject(& &1.allow_nil?)
|> Enum.map(&AshJsonApi.Resource.Info.argument_to_json_key(resource, action.name, &1.name))
end

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

Expand Down
161 changes: 161 additions & 0 deletions test/acceptance/json_schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,120 @@ defmodule Test.Acceptance.JsonSchemaTest do
end
end

defmodule HiddenSpecAuthor do
use Ash.Resource,
domain: Test.Acceptance.JsonSchemaTest.HiddenSpecDomain,
data_layer: Ash.DataLayer.Ets,
extensions: [
AshJsonApi.Resource
]

ets do
private?(true)
end

json_api do
type("hidden-json-author")

routes do
base("/hidden_json_authors")
index(:read)
end
end

actions do
default_accept(:*)
defaults([:read, :create])
end

attributes do
uuid_primary_key(:id, writable?: true, public?: true)
attribute(:name, :string, allow_nil?: false, public?: true)
end
end

defmodule HiddenSpecPost do
use Ash.Resource,
domain: Test.Acceptance.JsonSchemaTest.HiddenSpecDomain,
data_layer: Ash.DataLayer.Ets,
extensions: [
AshJsonApi.Resource
]

ets do
private?(true)
end

json_api do
type("hidden-json-post")
default_fields([:name, :secret, :secret_calc])
hide_fields([:secret, :secret_calc, :hidden_author])

routes do
base("/hidden_json_posts")
get(:read)
index(:read)
post(:create, relationship_arguments: [{:id, :visible_author}, {:id, :hidden_author}])
related(:visible_author, :read)
related(:hidden_author, :read)
end
end

actions do
default_accept(:*)
defaults([:read])

create :create do
primary? true
accept([:id, :name, :secret])
argument(:visible_author, :uuid, allow_nil?: false)
argument(:hidden_author, :uuid, allow_nil?: false)

change(manage_relationship(:visible_author, type: :append_and_remove))
change(manage_relationship(:hidden_author, type: :append_and_remove))
end
end

attributes do
uuid_primary_key(:id, writable?: true, public?: true)
attribute(:name, :string, allow_nil?: false, public?: true)
attribute(:secret, :string, allow_nil?: false, public?: true)
end

calculations do
calculate(:secret_calc, :string, concat([:name, :name], "-"), public?: true)
end

relationships do
belongs_to(:visible_author, Test.Acceptance.JsonSchemaTest.HiddenSpecAuthor,
allow_nil?: false,
public?: true
)

belongs_to(:hidden_author, Test.Acceptance.JsonSchemaTest.HiddenSpecAuthor,
allow_nil?: false,
public?: true
)
end
end

defmodule HiddenSpecDomain do
use Ash.Domain,
otp_app: :ash_json_api,
extensions: [
AshJsonApi.Domain
]

json_api do
log_errors?(false)
end

resources do
resource(HiddenSpecAuthor)
resource(HiddenSpecPost)
end
end

defmodule Blogs do
use Ash.Domain,
otp_app: :ash_json_api,
Expand Down Expand Up @@ -189,6 +303,53 @@ defmodule Test.Acceptance.JsonSchemaTest do
)
end

test "hide_fields hides fields from the generated JSON schema" do
json_api = AshJsonApi.JsonSchema.generate([HiddenSpecDomain])

post_schema = json_api["definitions"]["hidden-json-post"]
attributes = post_schema["properties"]["attributes"]["properties"]
relationships = post_schema["properties"]["relationships"]["properties"]

assert Map.has_key?(attributes, "name")
refute Map.has_key?(attributes, "secret")
refute Map.has_key?(attributes, "secret_calc")
refute "secret" in post_schema["properties"]["attributes"]["required"]

assert Map.has_key?(relationships, "visible_author")
refute Map.has_key?(relationships, "hidden_author")

hrefs = Enum.map(json_api["links"], & &1["href"])
assert Enum.any?(hrefs, &String.contains?(&1, "/hidden_json_posts/{id}/visible_author"))
refute Enum.any?(hrefs, &String.contains?(&1, "/hidden_json_posts/{id}/hidden_author"))

index_link =
Enum.find(json_api["links"], fn link ->
link["rel"] == "index" && String.starts_with?(link["href"], "/hidden_json_posts")
end)

assert index_link["hrefSchema"]["properties"]["sort"]["format"] =~ "name"
refute index_link["hrefSchema"]["properties"]["sort"]["format"] =~ "secret"

create_link =
Enum.find(json_api["links"], fn link ->
link["rel"] == "post" && String.starts_with?(link["href"], "/hidden_json_posts")
end)

create_attributes = create_link["schema"]["properties"]["data"]["properties"]["attributes"]

create_relationships =
create_link["schema"]["properties"]["data"]["properties"]["relationships"]

assert Map.has_key?(create_attributes["properties"], "name")
refute Map.has_key?(create_attributes["properties"], "secret")
refute "secret" in create_attributes["required"]

assert Map.has_key?(create_relationships["properties"], "visible_author")
refute Map.has_key?(create_relationships["properties"], "hidden_author")
assert "visible_author" in create_relationships["required"]
refute "hidden_author" in create_relationships["required"]
end

test "handles self-referential embedded resources without infinite loop" do
# This should complete without timing out
# If it loops infinitely, the test will timeout
Expand Down
Loading