Skip to content

Commit 231baff

Browse files
authored
fix: Filter hidden fields from result when using the field hiding dsl (#442)
1 parent a2ed3ec commit 231baff

10 files changed

Lines changed: 537 additions & 71 deletions

File tree

documentation/dsls/DSL-AshJsonApi.Resource.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ end
6767
| [`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). |
6868
| [`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 |
6969
| [`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 |
70-
| [`hide_fields`](#json_api-hide_fields){: #json_api-hide_fields } | `list(atom)` | `[]` | A list of fields to hide from the generated OpenAPI specification. Applies to attributes, relationships, calculations, and aggregates. |
71-
| [`show_fields`](#json_api-show_fields){: #json_api-show_fields } | `list(atom)` | | A list of fields to show in the generated OpenAPI specification. If not specified, all public fields are shown except those in `hide_fields`. |
70+
| [`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. |
71+
| [`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`. |
7272
| [`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 |
7373
| [`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 |
7474
| [`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. |

lib/ash_json_api/domain/persisters/define_router.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ defmodule AshJsonApi.Domain.Persisters.DefineRouter do
99
alias Spark.Dsl.Transformer
1010

1111
def transform(dsl) do
12-
routes = AshJsonApi.Domain.Info.routes(dsl)
12+
routes =
13+
dsl
14+
|> AshJsonApi.Domain.Info.routes()
15+
|> Enum.filter(&route_visible?/1)
1316

1417
route_matchers =
1518
routes
@@ -45,6 +48,12 @@ defmodule AshJsonApi.Domain.Persisters.DefineRouter do
4548
)}
4649
end
4750

51+
defp route_visible?(%{relationship: nil}), do: true
52+
53+
defp route_visible?(%{resource: resource, relationship: relationship}) do
54+
AshJsonApi.Resource.Info.show_field?(resource, relationship)
55+
end
56+
4857
# sobelow_skip ["DOS.StringToAtom"]
4958
def route_match(route) do
5059
split_route = String.split(route.route, "/", trim: true)

lib/ash_json_api/includes/parser.ex

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,48 @@ defmodule AshJsonApi.Includes.Parser do
3030
defp allowed_preloads(resource) do
3131
resource
3232
|> AshJsonApi.Resource.Info.includes()
33+
|> filter_hidden_includes(resource)
3334
|> to_nested_map()
3435
end
3536

37+
defp filter_hidden_includes(includes, resource) when is_list(includes) do
38+
includes
39+
|> Enum.flat_map(fn include ->
40+
case filter_hidden_include(include, resource) do
41+
nil -> []
42+
include -> [include]
43+
end
44+
end)
45+
end
46+
47+
defp filter_hidden_include({include, further}, resource) do
48+
case public_relationship(resource, include) do
49+
%{destination: destination} ->
50+
{include, filter_hidden_includes(List.wrap(further), destination)}
51+
52+
nil ->
53+
nil
54+
end
55+
end
56+
57+
defp filter_hidden_include(include, resource) do
58+
if public_relationship(resource, include) do
59+
include
60+
end
61+
end
62+
63+
defp public_relationship(resource, relationship_name) do
64+
case Ash.Resource.Info.public_relationship(resource, relationship_name) do
65+
%{name: name} = relationship ->
66+
if AshJsonApi.Resource.Info.show_field?(resource, name) do
67+
relationship
68+
end
69+
70+
nil ->
71+
nil
72+
end
73+
end
74+
3675
defp to_nested_map(list) when is_list(list) do
3776
list
3877
|> Enum.map(fn

lib/ash_json_api/request.ex

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -687,22 +687,28 @@ defmodule AshJsonApi.Request do
687687

688688
defp parse_field_inputs(request), do: request
689689

690-
if function_exported?(Ash.Resource.Info, :public_related, 2) do
691-
defp public_related(resource, relationship) do
692-
Ash.Resource.Info.public_related(resource, relationship)
693-
end
694-
else
695-
defp public_related(resource, relationship) when not is_list(relationship) do
696-
public_related(resource, [relationship])
690+
defp public_related(resource, relationship) when not is_list(relationship) do
691+
public_related(resource, [relationship])
692+
end
693+
694+
defp public_related(resource, []), do: resource
695+
696+
defp public_related(resource, [path | rest]) do
697+
case public_relationship(resource, path) do
698+
%{destination: destination} -> public_related(destination, rest)
699+
nil -> nil
697700
end
701+
end
698702

699-
defp public_related(resource, []), do: resource
703+
defp public_relationship(resource, relationship_name) do
704+
case Ash.Resource.Info.public_relationship(resource, relationship_name) do
705+
%{name: name} = relationship ->
706+
if AshJsonApi.Resource.Info.show_field?(resource, name) do
707+
relationship
708+
end
700709

701-
defp public_related(resource, [path | rest]) do
702-
case Ash.Resource.Info.public_relationship(resource, path) do
703-
%{destination: destination} -> public_related(destination, rest)
704-
nil -> nil
705-
end
710+
nil ->
711+
nil
706712
end
707713
end
708714

@@ -730,48 +736,56 @@ defmodule AshJsonApi.Request do
730736
)
731737

732738
calculation ->
733-
Enum.reduce(arguments, request, fn {arg_name, arg_value}, request ->
734-
calc_arg_names = AshJsonApi.Resource.Info.calculation_argument_names(resource)
735-
736-
calculation_arg =
737-
Enum.find(calculation.arguments, fn argument ->
738-
AshJsonApi.Resource.Info.apply_argument_name_mapping(
739-
calc_arg_names,
740-
calculation.name,
741-
argument.name
742-
) == arg_name
743-
end)
739+
if AshJsonApi.Resource.Info.show_field?(resource, calculation.name) do
740+
Enum.reduce(arguments, request, fn {arg_name, arg_value}, request ->
741+
calc_arg_names = AshJsonApi.Resource.Info.calculation_argument_names(resource)
742+
743+
calculation_arg =
744+
Enum.find(calculation.arguments, fn argument ->
745+
AshJsonApi.Resource.Info.apply_argument_name_mapping(
746+
calc_arg_names,
747+
calculation.name,
748+
argument.name
749+
) == arg_name
750+
end)
744751

745-
case calculation_arg do
746-
nil ->
747-
add_error(
748-
request,
749-
InvalidField.exception(type: type, parameter?: true, field: arg_name),
750-
request.route.type
751-
)
752+
case calculation_arg do
753+
nil ->
754+
add_error(
755+
request,
756+
InvalidField.exception(type: type, parameter?: true, field: arg_name),
757+
request.route.type
758+
)
752759

753-
_ ->
754-
cur_resource_field_inputs = Map.get(request.field_inputs, resource, %{})
760+
_ ->
761+
cur_resource_field_inputs = Map.get(request.field_inputs, resource, %{})
755762

756-
cur_calculation_field_inputs =
757-
Map.get(cur_resource_field_inputs, calculation.name, %{})
763+
cur_calculation_field_inputs =
764+
Map.get(cur_resource_field_inputs, calculation.name, %{})
758765

759-
updated_calculation_field_inputs =
760-
Map.put(cur_calculation_field_inputs, calculation_arg.name, arg_value)
766+
updated_calculation_field_inputs =
767+
Map.put(cur_calculation_field_inputs, calculation_arg.name, arg_value)
761768

762-
updated_resource_field_inputs =
763-
Map.put(
764-
cur_resource_field_inputs,
765-
calculation.name,
766-
updated_calculation_field_inputs
767-
)
769+
updated_resource_field_inputs =
770+
Map.put(
771+
cur_resource_field_inputs,
772+
calculation.name,
773+
updated_calculation_field_inputs
774+
)
768775

769-
updated_field_inputs =
770-
Map.put(request.field_inputs, resource, updated_resource_field_inputs)
776+
updated_field_inputs =
777+
Map.put(request.field_inputs, resource, updated_resource_field_inputs)
771778

772-
%{request | field_inputs: updated_field_inputs}
773-
end
774-
end)
779+
%{request | field_inputs: updated_field_inputs}
780+
end
781+
end)
782+
else
783+
add_error(
784+
request,
785+
InvalidField.exception(type: type, parameter?: true, field: calculation_name),
786+
request.route.type
787+
)
788+
end
775789
end
776790
end)
777791
end
@@ -788,20 +802,22 @@ defmodule AshJsonApi.Request do
788802
resource
789803
|> Ash.Resource.Info.public_attributes()
790804
|> Enum.find(fn a ->
791-
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) == key
805+
AshJsonApi.Resource.Info.show_field?(resource, a.name) &&
806+
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) == key
792807
end) ->
793808
fields = Map.update(request.fields, resource, [attr.name], &[attr.name | &1])
794809
%{request | fields: fields}
795810

796-
rel = Ash.Resource.Info.public_relationship(resource, key) ->
811+
rel = public_relationship(resource, key) ->
797812
fields = Map.update(request.fields, resource, [rel.name], &[rel.name | &1])
798813
%{request | fields: fields}
799814

800815
agg =
801816
resource
802817
|> Ash.Resource.Info.public_aggregates()
803818
|> Enum.find(fn a ->
804-
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) == key
819+
AshJsonApi.Resource.Info.show_field?(resource, a.name) &&
820+
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) == key
805821
end) ->
806822
fields = Map.update(request.fields, resource, [agg.name], &[agg.name | &1])
807823
%{request | fields: fields}
@@ -810,7 +826,8 @@ defmodule AshJsonApi.Request do
810826
resource
811827
|> Ash.Resource.Info.public_calculations()
812828
|> Enum.find(fn c ->
813-
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, c.name) == key
829+
AshJsonApi.Resource.Info.show_field?(resource, c.name) &&
830+
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, c.name) == key
814831
end) ->
815832
fields = Map.update(request.fields, resource, [calc.name], &[calc.name | &1])
816833
%{request | fields: fields}
@@ -873,26 +890,29 @@ defmodule AshJsonApi.Request do
873890
resource
874891
|> Ash.Resource.Info.public_attributes()
875892
|> Enum.find(fn a ->
876-
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) ==
877-
field_name
893+
AshJsonApi.Resource.Info.show_field?(resource, a.name) &&
894+
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) ==
895+
field_name
878896
end) ->
879897
%{request | sort: [{attr.name, order} | request.sort]}
880898

881899
agg =
882900
resource
883901
|> Ash.Resource.Info.public_aggregates()
884902
|> Enum.find(fn a ->
885-
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) ==
886-
field_name
903+
AshJsonApi.Resource.Info.show_field?(resource, a.name) &&
904+
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, a.name) ==
905+
field_name
887906
end) ->
888907
%{request | sort: [{agg.name, order} | request.sort]}
889908

890909
calc =
891910
resource
892911
|> Ash.Resource.Info.public_calculations()
893912
|> Enum.find(fn c ->
894-
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, c.name) ==
895-
field_name
913+
AshJsonApi.Resource.Info.show_field?(resource, c.name) &&
914+
AshJsonApi.Resource.Info.apply_field_name_mapping(attr_names, c.name) ==
915+
field_name
896916
end) ->
897917
%{request | sort: [{calc.name, order} | request.sort]}
898918

@@ -981,7 +1001,9 @@ defmodule AshJsonApi.Request do
9811001
) do
9821002
includes =
9831003
Enum.reduce(
984-
AshJsonApi.Resource.Info.always_include_linkage(resource),
1004+
resource
1005+
|> AshJsonApi.Resource.Info.always_include_linkage()
1006+
|> filter_shown_fields(resource),
9851007
includes,
9861008
fn key, includes ->
9871009
if Keyword.has_key?(includes, key) do
@@ -1018,6 +1040,7 @@ defmodule AshJsonApi.Request do
10181040
|> Ash.Resource.Info.public_attributes()
10191041
|> Enum.map(& &1.name)
10201042
)
1043+
|> filter_shown_fields(related)
10211044
|> Enum.map(fn field ->
10221045
case Map.get(related_field_inputs, field) do
10231046
nil -> field
@@ -1418,6 +1441,10 @@ defmodule AshJsonApi.Request do
14181441
{:error, InvalidRelationshipInput.exception(relationship: name, input: value)}
14191442
end
14201443

1444+
defp filter_shown_fields(fields, resource) do
1445+
Enum.filter(fields, &AshJsonApi.Resource.Info.show_field?(resource, &1))
1446+
end
1447+
14211448
defp url(conn) do
14221449
Conn.request_url(conn)
14231450
end

lib/ash_json_api/resource/info.ex

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,21 @@ defmodule AshJsonApi.Resource.Info do
7070
Extension.get_opt(resource, [:json_api], :default_fields, nil, true)
7171
end
7272

73-
@doc "Fields to hide from the generated API specification"
73+
@doc "Fields to hide from generated API specifications and JSON:API responses"
7474
def hide_fields(resource) do
7575
Extension.get_opt(resource, [:json_api], :hide_fields, [], true)
7676
end
7777

78-
@doc "Fields to show in the generated API specification"
78+
@doc "Fields to show in generated API specifications and JSON:API responses"
7979
def show_fields(resource) do
8080
Extension.get_opt(resource, [:json_api], :show_fields, nil, true)
8181
end
8282

83-
@doc "Whether or not a given field should be shown in the generated API specification"
83+
@doc "Whether or not a given field should be exposed in JSON:API"
84+
def show_field?(resource, %{name: name}) do
85+
show_field?(resource, name)
86+
end
87+
8488
def show_field?(resource, field) do
8589
hide_fields = hide_fields(resource)
8690
show_fields = show_fields(resource) || [field]
@@ -189,7 +193,10 @@ defmodule AshJsonApi.Resource.Info do
189193
Ash.Resource.Info.public_aggregates(resource)
190194

191195
Enum.find_value(all_fields, fn field ->
192-
if apply_field_name_mapping(names, field.name) == json_key, do: field.name
196+
if show_field?(resource, field.name) &&
197+
apply_field_name_mapping(names, field.name) == json_key do
198+
field.name
199+
end
193200
end)
194201
end
195202

lib/ash_json_api/resource/persisters/define_router.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ defmodule AshJsonApi.Resource.Persisters.DefineRouter do
99
alias Spark.Dsl.Transformer
1010

1111
def transform(dsl) do
12-
routes = AshJsonApi.Resource.Info.routes(dsl)
12+
routes =
13+
dsl
14+
|> AshJsonApi.Resource.Info.routes()
15+
|> Enum.filter(&route_visible?(dsl, &1))
1316

1417
route_matchers =
1518
routes
@@ -45,6 +48,12 @@ defmodule AshJsonApi.Resource.Persisters.DefineRouter do
4548
)}
4649
end
4750

51+
defp route_visible?(_resource, %{relationship: nil}), do: true
52+
53+
defp route_visible?(resource, %{relationship: relationship}) do
54+
AshJsonApi.Resource.Info.show_field?(resource, relationship)
55+
end
56+
4857
# sobelow_skip ["DOS.StringToAtom"]
4958
def route_match(route) do
5059
split_route = String.split(route.route, "/", trim: true)

0 commit comments

Comments
 (0)