diff --git a/documentation/dsls/DSL-AshJsonApi.Resource.md b/documentation/dsls/DSL-AshJsonApi.Resource.md index 1350779..15c85d9 100644 --- a/documentation/dsls/DSL-AshJsonApi.Resource.md +++ b/documentation/dsls/DSL-AshJsonApi.Resource.md @@ -67,8 +67,8 @@ end | [`paginated_includes`](#json_api-paginated_includes){: #json_api-paginated_includes } | `list(atom \| list(atom))` | `[]` | A list of relationship paths that can be paginated when included via the `included_page` query parameter. Each entry can be either an atom (for top-level relationships) or a list of atoms (for nested paths). | | [`include_nil_values?`](#json_api-include_nil_values?){: #json_api-include_nil_values? } | `any` | | Whether or not to include properties for values that are nil in the JSON output | | [`default_fields`](#json_api-default_fields){: #json_api-default_fields } | `list(atom)` | | The fields to include in the object if the `fields` query parameter does not specify. Defaults to all public | -| [`hide_fields`](#json_api-hide_fields){: #json_api-hide_fields } | `list(atom)` | `[]` | A list of fields to hide from generated API specifications and JSON:API responses. Applies to attributes, relationships, calculations, and aggregates. | -| [`show_fields`](#json_api-show_fields){: #json_api-show_fields } | `list(atom)` | | A list of fields to show in generated API specifications and JSON:API responses. If not specified, all public fields are shown except those in `hide_fields`. | +| [`hide_fields`](#json_api-hide_fields){: #json_api-hide_fields } | `list(atom)` | `[]` | A list of fields to hide from generated API specifications and JSON:API responses. Applies to attributes, relationships, calculations, and aggregates. Takes precedence over `show_fields`. | +| [`show_fields`](#json_api-show_fields){: #json_api-show_fields } | `list(atom)` | | A list of fields to show in generated API specifications and JSON:API responses. If not specified, all public fields are shown except those in `hide_fields`. Fields in `hide_fields` are hidden even if listed here. | | [`derive_sort?`](#json_api-derive_sort?){: #json_api-derive_sort? } | `boolean` | `true` | Whether or not to derive a sort parameter based on the sortable fields of the resource | | [`derive_filter?`](#json_api-derive_filter?){: #json_api-derive_filter? } | `boolean` | `true` | Whether or not to derive a filter parameter based on the sortable fields of the resource | | [`relationship_meta_in`](#json_api-relationship_meta_in){: #json_api-relationship_meta_in } | `keyword` | `[]` | Configures how incoming JSON:API `meta` keys on relationship resource identifiers map to join resource attributes for many_to_many relationship writes. Use together with `relationship_meta_out` for reads. Each relationship you want to support must declare both mappings explicitly. | diff --git a/lib/ash_json_api/resource/info.ex b/lib/ash_json_api/resource/info.ex index 42fc58e..888e54b 100644 --- a/lib/ash_json_api/resource/info.ex +++ b/lib/ash_json_api/resource/info.ex @@ -75,7 +75,7 @@ defmodule AshJsonApi.Resource.Info do Extension.get_opt(resource, [:json_api], :hide_fields, [], true) end - @doc "Fields to show in generated API specifications and JSON:API responses" + @doc "Fields to show in generated API specifications and JSON:API responses. `hide_fields` takes precedence." def show_fields(resource) do Extension.get_opt(resource, [:json_api], :show_fields, nil, true) end diff --git a/lib/ash_json_api/resource/resource.ex b/lib/ash_json_api/resource/resource.ex index b255c79..7327441 100644 --- a/lib/ash_json_api/resource/resource.ex +++ b/lib/ash_json_api/resource/resource.ex @@ -561,12 +561,12 @@ defmodule AshJsonApi.Resource do type: {:list, :atom}, default: [], doc: - "A list of fields to hide from generated API specifications and JSON:API responses. Applies to attributes, relationships, calculations, and aggregates." + "A list of fields to hide from generated API specifications and JSON:API responses. Applies to attributes, relationships, calculations, and aggregates. Takes precedence over `show_fields`." ], show_fields: [ type: {:list, :atom}, doc: - "A list of fields to show in generated API specifications and JSON:API responses. If not specified, all public fields are shown except those in `hide_fields`." + "A list of fields to show in generated API specifications and JSON:API responses. If not specified, all public fields are shown except those in `hide_fields`. Fields in `hide_fields` are hidden even if listed here." ], derive_sort?: [ type: :boolean, diff --git a/lib/ash_json_api/resource/verifiers/verify_field_references.ex b/lib/ash_json_api/resource/verifiers/verify_field_references.ex index a7286d7..a11097a 100644 --- a/lib/ash_json_api/resource/verifiers/verify_field_references.ex +++ b/lib/ash_json_api/resource/verifiers/verify_field_references.ex @@ -22,7 +22,6 @@ defmodule AshJsonApi.Resource.Verifiers.VerifyFieldReferences do validate_fields!(resource, :show_fields, show_fields, public_fields) validate_fields!(resource, :hide_fields, hide_fields, public_fields) - validate_show_hide_overlap!(resource, show_fields, hide_fields) :ok end @@ -43,26 +42,4 @@ defmodule AshJsonApi.Resource.Verifiers.VerifyFieldReferences do end end) end - - defp validate_show_hide_overlap!(_resource, nil, _hide_fields), do: :ok - - defp validate_show_hide_overlap!(resource, show_fields, hide_fields) do - overlap = - show_fields - |> MapSet.new() - |> MapSet.intersection(MapSet.new(hide_fields || [])) - |> MapSet.to_list() - |> Enum.sort() - - unless Enum.empty?(overlap) do - raise Spark.Error.DslError, - module: resource, - path: [:json_api], - message: """ - Fields cannot appear in both `show_fields` and `hide_fields`. - - Conflicting fields: #{inspect(overlap)} - """ - end - end end diff --git a/test/acceptance/field_visibility_test.exs b/test/acceptance/field_visibility_test.exs index e9fea1a..013b2e1 100644 --- a/test/acceptance/field_visibility_test.exs +++ b/test/acceptance/field_visibility_test.exs @@ -119,7 +119,8 @@ defmodule Test.Acceptance.FieldVisibilityTest do type("visibility-show-post") includes([:visible_author, :extra_author]) default_fields([:title, :summary, :secret_calc]) - show_fields([:title, :visible_author]) + hide_fields([:summary]) + show_fields([:title, :summary, :visible_author]) routes do base("/visibility_show_posts") @@ -312,7 +313,9 @@ defmodule Test.Acceptance.FieldVisibilityTest do assert response.resp_body["data"]["type"] == "visibility-author" end - test "show_fields only exposes allowlisted fields", %{show_only_post: post} do + test "show_fields only exposes allowlisted fields and hide_fields wins", %{ + show_only_post: post + } do response = Domain |> get("/visibility_show_posts/#{post.id}", status: 200) @@ -329,6 +332,18 @@ defmodule Test.Acceptance.FieldVisibilityTest do end test "show_fields rejects non-allowlisted sparse fieldsets", %{show_only_post: post} do + Domain + |> get("/visibility_show_posts/#{post.id}?fields[visibility-show-post]=secret_calc", + status: 400 + ) + |> assert_has_error(%{ + "code" => "invalid_field" + }) + end + + test "hide_fields rejects sparse fieldsets even when the field is in show_fields", %{ + show_only_post: post + } do Domain |> get("/visibility_show_posts/#{post.id}?fields[visibility-show-post]=summary", status: 400) |> assert_has_error(%{ diff --git a/test/acceptance/json_schema_test.exs b/test/acceptance/json_schema_test.exs index 8733f2b..d1a4e5c 100644 --- a/test/acceptance/json_schema_test.exs +++ b/test/acceptance/json_schema_test.exs @@ -199,6 +199,7 @@ defmodule Test.Acceptance.JsonSchemaTest do type("hidden-json-post") default_fields([:name, :secret, :secret_calc]) hide_fields([:secret, :secret_calc, :hidden_author]) + show_fields([:name, :secret, :secret_calc, :visible_author, :hidden_author]) routes do base("/hidden_json_posts") diff --git a/test/acceptance/open_api_test.exs b/test/acceptance/open_api_test.exs index 210af21..1317711 100644 --- a/test/acceptance/open_api_test.exs +++ b/test/acceptance/open_api_test.exs @@ -316,6 +316,7 @@ defmodule Test.Acceptance.OpenApiTest do paginated_includes([:visible_author, :hidden_author]) default_fields([:name, :secret, :secret_calc]) hide_fields([:secret, :secret_calc, :hidden_author]) + show_fields([:name, :secret, :secret_calc, :visible_author, :hidden_author]) routes do base("/hidden_spec_posts")