Skip to content

Commit af05f2a

Browse files
committed
Add field visibility DSL
1 parent cdd5559 commit af05f2a

7 files changed

Lines changed: 382 additions & 15 deletions

File tree

.formatter.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ spark_locals_without_parens = [
2929
get: 2,
3030
get: 3,
3131
group_by: 1,
32+
hide_fields: 1,
3233
include_nil_values?: 1,
3334
includes: 1,
3435
index: 1,
@@ -75,6 +76,7 @@ spark_locals_without_parens = [
7576
route: 5,
7677
router: 1,
7778
serve_schema?: 1,
79+
show_fields: 1,
7880
show_raised_errors?: 1,
7981
tag: 1,
8082
type: 1,

documentation/dsls/DSL-AshJsonApi.Resource.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +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`. |
7072
| [`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 |
7173
| [`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 |
7274
| [`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/json_schema/open_api.ex

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,12 @@ if Code.ensure_loaded?(OpenApiSpex) do
198198
defp all_resources_requiring_filter_schemas(domains) do
199199
domains
200200
|> Enum.flat_map(&Ash.Domain.Info.resources/1)
201-
|> Enum.reject(&Enum.empty?(AshJsonApi.Resource.Info.routes(&1, domains)))
201+
|> Enum.reject(fn resource ->
202+
resource
203+
|> AshJsonApi.Resource.Info.routes(domains)
204+
|> Enum.filter(&route_visible?(resource, &1))
205+
|> Enum.empty?()
206+
end)
202207
|> with_all_related_resources()
203208
|> Enum.filter(fn resource ->
204209
AshJsonApi.Resource.Info.type(resource) &&
@@ -209,7 +214,11 @@ if Code.ensure_loaded?(OpenApiSpex) do
209214
defp with_all_related_resources(resources, checked \\ []) do
210215
resources
211216
|> Enum.reject(&(&1 in checked))
212-
|> Enum.flat_map(&Ash.Resource.Info.public_relationships/1)
217+
|> Enum.flat_map(fn resource ->
218+
resource
219+
|> Ash.Resource.Info.public_relationships()
220+
|> filter_shown_fields(resource)
221+
end)
213222
|> Enum.map(& &1.destination)
214223
|> Enum.reject(&(&1 in resources))
215224
|> case do
@@ -233,7 +242,11 @@ if Code.ensure_loaded?(OpenApiSpex) do
233242
domains
234243
|> Stream.flat_map(&Ash.Domain.Info.resources/1)
235244
|> Stream.reject(&Enum.empty?(AshJsonApi.Resource.Info.routes(&1, domains)))
236-
|> Stream.flat_map(&Ash.Resource.Info.relationships/1)
245+
|> Stream.flat_map(fn resource ->
246+
resource
247+
|> Ash.Resource.Info.relationships()
248+
|> filter_shown_fields(resource)
249+
end)
237250
|> Stream.filter(& &1.public?)
238251
|> Enum.any?(&(&1.destination == resource))
239252
end
@@ -252,6 +265,18 @@ if Code.ensure_loaded?(OpenApiSpex) do
252265
action && action.type == :read
253266
end
254267

268+
defp show_field?(resource, %{name: name}) do
269+
AshJsonApi.Resource.Info.show_field?(resource, name)
270+
end
271+
272+
defp show_field?(resource, field) do
273+
AshJsonApi.Resource.Info.show_field?(resource, field)
274+
end
275+
276+
defp filter_shown_fields(fields, resource) do
277+
Enum.filter(fields, &show_field?(resource, &1))
278+
end
279+
255280
defp resource_filter_schemas(domains, resource, acc) do
256281
{field_types, acc} = filter_field_types(resource, acc)
257282

@@ -381,6 +406,8 @@ if Code.ensure_loaded?(OpenApiSpex) do
381406
fields || AshJsonApi.Resource.Info.default_fields(resource) ||
382407
Enum.map(Ash.Resource.Info.public_attributes(resource), & &1.name)
383408

409+
fields = Enum.filter(fields, &show_field?(resource, &1))
410+
384411
{properties, acc} = resource_attributes(resource, fields, :json, acc)
385412

386413
schema =
@@ -399,6 +426,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
399426
defp required_attributes(resource) do
400427
resource
401428
|> Ash.Resource.Info.public_attributes()
429+
|> filter_shown_fields(resource)
402430
|> Enum.reject(&(&1.allow_nil? || AshJsonApi.Resource.only_primary_key?(resource, &1.name)))
403431
|> Enum.map(fn attr ->
404432
AshJsonApi.Resource.Info.field_to_json_key(resource, attr.name)
@@ -420,6 +448,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
420448
Ash.Resource.Info.public_aggregates(resource)
421449
|> AshJsonApi.JsonSchema.set_aggregate_constraints(resource)
422450
)
451+
|> filter_shown_fields(resource)
423452
|> Enum.map(fn
424453
%Ash.Resource.Aggregate{} = agg ->
425454
field =
@@ -1343,6 +1372,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
13431372
defp resource_relationships(resource) do
13441373
resource
13451374
|> Ash.Resource.Info.public_relationships()
1375+
|> filter_shown_fields(resource)
13461376
|> Enum.filter(fn %{destination: destination} ->
13471377
AshJsonApi.Resource.Info.type(destination)
13481378
end)
@@ -1517,7 +1547,10 @@ if Code.ensure_loaded?(OpenApiSpex) do
15171547
domain
15181548
|> resources()
15191549
|> Enum.flat_map_reduce(acc, fn resource, acc ->
1520-
routes = AshJsonApi.Resource.Info.routes(resource, all_domains)
1550+
routes =
1551+
resource
1552+
|> AshJsonApi.Resource.Info.routes(all_domains)
1553+
|> Enum.filter(&route_visible?(resource, &1))
15211554

15221555
{route_operations, acc} =
15231556
Enum.map_reduce(routes, acc, fn route, acc ->
@@ -1535,6 +1568,12 @@ if Code.ensure_loaded?(OpenApiSpex) do
15351568
{final_paths, final_acc}
15361569
end
15371570

1571+
defp route_visible?(_resource, %{relationship: nil}), do: true
1572+
1573+
defp route_visible?(resource, %{relationship: relationship}) do
1574+
show_field?(resource, relationship)
1575+
end
1576+
15381577
@spec route_operation(
15391578
Route.t(),
15401579
domain :: module,
@@ -1745,6 +1784,13 @@ if Code.ensure_loaded?(OpenApiSpex) do
17451784
end)
17461785
)
17471786
|> Enum.filter(& &1)
1787+
|> Enum.reject(fn
1788+
%Ash.Resource.Attribute{name: name} ->
1789+
!show_field?(resource, name) && to_string(name) not in route_params
1790+
1791+
_ ->
1792+
false
1793+
end)
17481794
|> Enum.reduce({[], acc}, fn argument_or_attribute, {list, acc} ->
17491795
{schema, acc} =
17501796
resource_write_attribute_type(argument_or_attribute, resource, action.type, acc)
@@ -1859,6 +1905,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
18591905
sorts =
18601906
resource
18611907
|> AshJsonApi.JsonSchema.sortable_fields()
1908+
|> filter_shown_fields(resource)
18621909
|> Enum.flat_map(fn attr ->
18631910
name = AshJsonApi.Resource.Info.field_to_json_key(resource, attr.name)
18641911
[name, "-" <> name, "\\+\\+" <> name, "--" <> name]
@@ -1992,6 +2039,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
19922039
resource
19932040
|> AshJsonApi.Resource.Info.includes()
19942041
|> all_paths()
2042+
|> Enum.filter(&visible_relationship_path?(resource, &1))
19952043
|> Enum.map(&Enum.join(&1, "."))
19962044

19972045
%Parameter{
@@ -2016,6 +2064,10 @@ if Code.ensure_loaded?(OpenApiSpex) do
20162064
# Get all paginated relationship paths
20172065
paginated_paths =
20182066
paginated_includes
2067+
|> Enum.filter(fn
2068+
path when is_list(path) -> visible_relationship_path?(resource, path)
2069+
atom when is_atom(atom) -> visible_relationship_path?(resource, [atom])
2070+
end)
20192071
|> Enum.map(fn
20202072
path when is_list(path) -> Enum.join(path, ".")
20212073
atom when is_atom(atom) -> to_string(atom)
@@ -2041,11 +2093,10 @@ if Code.ensure_loaded?(OpenApiSpex) do
20412093
type = AshJsonApi.Resource.Info.type(resource)
20422094

20432095
example =
2044-
Enum.join(
2045-
AshJsonApi.Resource.Info.default_fields(resource) ||
2046-
Enum.map(Ash.Resource.Info.public_attributes(resource), & &1.name),
2047-
","
2048-
)
2096+
(AshJsonApi.Resource.Info.default_fields(resource) ||
2097+
Enum.map(Ash.Resource.Info.public_attributes(resource), & &1.name))
2098+
|> Enum.filter(&show_field?(resource, &1))
2099+
|> Enum.join(",")
20492100

20502101
%Parameter{
20512102
name: :fields,
@@ -2070,6 +2121,26 @@ if Code.ensure_loaded?(OpenApiSpex) do
20702121
}
20712122
end
20722123

2124+
defp visible_relationship_path?(resource, path) do
2125+
path
2126+
|> List.wrap()
2127+
|> Enum.reduce_while(resource, fn relationship_name, current_resource ->
2128+
relationship = Ash.Resource.Info.public_relationship(current_resource, relationship_name)
2129+
2130+
cond do
2131+
is_nil(relationship) ->
2132+
{:halt, false}
2133+
2134+
!show_field?(current_resource, relationship_name) ->
2135+
{:halt, false}
2136+
2137+
true ->
2138+
{:cont, relationship.destination}
2139+
end
2140+
end)
2141+
|> then(&(&1 != false))
2142+
end
2143+
20732144
@spec read_argument_parameters(
20742145
Route.t(),
20752146
resource :: module,
@@ -2394,12 +2465,13 @@ if Code.ensure_loaded?(OpenApiSpex) do
23942465
[]
23952466

23962467
:update ->
2397-
action.require_attributes
2468+
Enum.filter(action.require_attributes, &show_field?(resource, &1))
23982469

23992470
_ ->
24002471
resource
24012472
|> Ash.Resource.Info.attributes()
24022473
|> Enum.filter(&(&1.name in action.accept && &1.writable?))
2474+
|> filter_shown_fields(resource)
24032475
|> Enum.reject(
24042476
&(&1.name in filtered_arguments || &1.allow_nil? || not is_nil(&1.default) ||
24052477
&1.generated? ||
@@ -2417,6 +2489,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
24172489

24182490
require_attributes =
24192491
Map.get(action, :require_attributes, [])
2492+
|> Enum.filter(&show_field?(resource, &1))
24202493
|> Enum.map(&AshJsonApi.Resource.Info.field_to_json_key(resource, &1))
24212494

24222495
Enum.uniq(attribute_names ++ argument_names ++ require_attributes)
@@ -2438,6 +2511,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
24382511
resource
24392512
|> Ash.Resource.Info.attributes()
24402513
|> Enum.filter(&(&1.name in action.accept && &1.writable?))
2514+
|> filter_shown_fields(resource)
24412515
|> Enum.reduce({%{}, acc}, fn attribute, {attrs, acc} ->
24422516
{schema, acc} =
24432517
resource_write_attribute_type(attribute, resource, action.type, acc, format)
@@ -2492,6 +2566,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
24922566
defp required_relationship_attributes(resource, relationship_arguments, action) do
24932567
action.arguments
24942568
|> Enum.filter(&has_relationship_argument?(relationship_arguments, &1.name))
2569+
|> filter_shown_fields(resource)
24952570
|> Enum.reject(& &1.allow_nil?)
24962571
|> Enum.map(fn arg ->
24972572
AshJsonApi.Resource.Info.argument_to_json_key(resource, action.name, arg.name)
@@ -2503,6 +2578,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
25032578
defp write_relationships(resource, relationship_arguments, action) do
25042579
action.arguments
25052580
|> Enum.filter(&has_relationship_argument?(relationship_arguments, &1.name))
2581+
|> filter_shown_fields(resource)
25062582
|> Map.new(fn argument ->
25072583
data = resource_write_relationship_field_data(resource, argument)
25082584

@@ -2747,11 +2823,15 @@ if Code.ensure_loaded?(OpenApiSpex) do
27472823
do: relationship_destination(resource, include) |> List.wrap()
27482824

27492825
defp relationship_destination(resource, include) do
2750-
resource
2751-
|> Ash.Resource.Info.public_relationship(include)
2752-
|> case do
2753-
%{destination: destination} -> destination
2754-
_ -> nil
2826+
if show_field?(resource, include) do
2827+
resource
2828+
|> Ash.Resource.Info.public_relationship(include)
2829+
|> case do
2830+
%{destination: destination} -> destination
2831+
_ -> nil
2832+
end
2833+
else
2834+
nil
27552835
end
27562836
end
27572837

@@ -2765,6 +2845,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
27652845
defp filter_attribute_types(resource, acc) do
27662846
resource
27672847
|> Ash.Resource.Info.public_attributes()
2848+
|> filter_shown_fields(resource)
27682849
|> Enum.filter(&filterable?(&1, resource))
27692850
|> Enum.reduce({[], acc}, fn attribute, {results, acc} ->
27702851
{result, acc} = filter_type(attribute, resource, acc)
@@ -2775,6 +2856,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
27752856
defp filter_aggregate_types(resource, acc) do
27762857
resource
27772858
|> Ash.Resource.Info.public_aggregates()
2859+
|> filter_shown_fields(resource)
27782860
|> Enum.filter(&filterable?(&1, resource))
27792861
|> Enum.reduce({[], acc}, fn aggregate, {results, acc} ->
27802862
{result, acc} = filter_type(aggregate, resource, acc)
@@ -2785,6 +2867,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
27852867
defp filter_calculation_types(resource, acc) do
27862868
resource
27872869
|> Ash.Resource.Info.public_calculations()
2870+
|> filter_shown_fields(resource)
27882871
|> Enum.filter(&filterable?(&1, resource))
27892872
|> Enum.reduce({[], acc}, fn calculation, {results, acc} ->
27902873
{result, acc} = filter_type(calculation, resource, acc)
@@ -2987,6 +3070,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
29873070

29883071
resource
29893072
|> Ash.Resource.Info.public_relationships()
3073+
|> filter_shown_fields(resource)
29903074
|> Enum.filter(
29913075
&(&1.destination in all_resources &&
29923076
AshJsonApi.Resource.Info.derive_filter?(&1.destination) &&
@@ -3006,6 +3090,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
30063090
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation) do
30073091
resource
30083092
|> Ash.Resource.Info.public_calculations()
3093+
|> filter_shown_fields(resource)
30093094
|> Enum.filter(&filterable?(&1, resource))
30103095
|> Enum.map(fn calculation ->
30113096
{calculation.name,
@@ -3021,6 +3106,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
30213106
defp attribute_filter_fields(resource) do
30223107
resource
30233108
|> Ash.Resource.Info.public_attributes()
3109+
|> filter_shown_fields(resource)
30243110
|> Enum.filter(&filterable?(&1, resource))
30253111
|> Enum.map(fn attribute ->
30263112
{attribute.name,
@@ -3034,6 +3120,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
30343120
if Ash.DataLayer.data_layer_can?(resource, :aggregate_filter) do
30353121
resource
30363122
|> Ash.Resource.Info.public_aggregates()
3123+
|> filter_shown_fields(resource)
30373124
|> Enum.filter(&filterable?(&1, resource))
30383125
|> Enum.map(fn aggregate ->
30393126
{aggregate.name,

lib/ash_json_api/resource/info.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ 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"
74+
def hide_fields(resource) do
75+
Extension.get_opt(resource, [:json_api], :hide_fields, [], true)
76+
end
77+
78+
@doc "Fields to show in the generated API specification"
79+
def show_fields(resource) do
80+
Extension.get_opt(resource, [:json_api], :show_fields, nil, true)
81+
end
82+
83+
@doc "Whether or not a given field should be shown in the generated API specification"
84+
def show_field?(resource, field) do
85+
hide_fields = hide_fields(resource)
86+
show_fields = show_fields(resource) || [field]
87+
88+
field not in hide_fields and field in show_fields
89+
end
90+
7391
@doc """
7492
Returns the `relationship_meta_in` config for the resource.
7593
"""

0 commit comments

Comments
 (0)