Packages
ash_graphql
1.9.0
1.10.0
1.9.4
1.9.3
1.9.2
1.9.1
1.9.0
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.5
1.0.0-rc.4
retired
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.9
0.26.8
0.26.7
0.26.6
0.26.5
0.26.4
0.26.3
0.26.2
0.26.0
0.25.13
0.25.12
0.25.10
0.25.9
0.25.8
0.25.7
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.13
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
retired
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0-rc.3
0.20.0-rc.2
0.20.0-rc.1
0.20.0-rc.0
0.19.0
0.18.0-rc0
0.17.5
0.17.5-rc0
0.17.4
0.17.2
0.17.1
0.17.0
0.16.28
0.16.27
0.16.26
0.16.25
0.16.24
0.16.23
0.16.22
0.16.21
0.16.20
0.16.18-rc5
0.16.18-rc4
0.16.18-rc3
0.16.18-rc2
0.16.18-rc1
0.16.18-rc0
0.16.17
0.16.16
0.16.15
0.16.14
0.16.13
0.16.12
0.16.11
0.16.10
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.5
0.12.4
0.12.3
0.12.1
0.12.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
The extension for building GraphQL APIs with Ash
Current section
Files
Jump to
Current section
Files
lib/resource/verifiers/verify_field_dependencies.ex
# SPDX-FileCopyrightText: 2020 ash_graphql contributors <https://github.com/ash-project/ash_graphql/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule AshGraphql.Resource.Verifiers.VerifyFieldDependencies do
# Validates cross-field dependencies between graphql DSL options.
# For example, a field in sortable_fields that is hidden via hide_fields
# will have no effect at runtime - this verifier warns about such cases.
@moduledoc false
use Spark.Dsl.Verifier
alias Spark.Dsl.Transformer
@impl true
def verify(dsl) do
resource = Transformer.get_persisted(dsl, :module)
show_fields = AshGraphql.Resource.Info.show_fields(dsl)
hide_fields = AshGraphql.Resource.Info.hide_fields(dsl)
# Hard error: show_fields and hide_fields must not overlap
validate_show_hide_contradiction!(resource, show_fields, hide_fields)
has_visibility_constraints = not (is_nil(show_fields) and is_nil(hide_fields))
explicit_relationships = Spark.Dsl.Extension.get_opt(dsl, [:graphql], :relationships, nil)
warnings =
[]
|> then(fn warnings ->
if has_visibility_constraints do
visible = compute_visible_fields(dsl, show_fields, hide_fields)
warnings
|> check_invisible_fields(dsl, resource, visible, :sortable_fields, "sortable_fields")
|> check_invisible_fields(
dsl,
resource,
visible,
:filterable_fields,
"filterable_fields"
)
|> check_invisible_fields(
dsl,
resource,
visible,
:nullable_fields,
"nullable_fields"
)
|> check_invisible_field_names(dsl, resource, visible)
|> check_invisible_relationships(dsl, resource, visible)
|> check_invisible_paginate_relationship_with(dsl, resource, visible)
|> check_invisible_attribute_input_types(dsl, resource, visible)
else
warnings
end
end)
|> then(fn warnings ->
if is_list(explicit_relationships) do
included_rels = MapSet.new(explicit_relationships)
warnings
|> check_excluded_relationship(
dsl,
resource,
included_rels,
:paginate_relationship_with
)
|> check_excluded_relationship(dsl, resource, included_rels, :filterable_fields)
|> check_excluded_relationship(dsl, resource, included_rels, :field_names)
|> check_excluded_relationship(dsl, resource, included_rels, :nullable_fields)
else
warnings
end
end)
case warnings do
[] -> :ok
list -> {:warn, list}
end
end
defp validate_show_hide_contradiction!(_resource, show_fields, hide_fields)
when is_nil(show_fields) or is_nil(hide_fields),
do: :ok
defp validate_show_hide_contradiction!(resource, show_fields, hide_fields) do
overlap =
MapSet.intersection(MapSet.new(show_fields), MapSet.new(hide_fields))
|> MapSet.to_list()
|> Enum.sort()
unless Enum.empty?(overlap) do
raise Spark.Error.DslError,
module: resource,
path: [:graphql],
message: """
Fields cannot appear in both `show_fields` and `hide_fields`.
Conflicting fields: #{inspect(overlap)}
"""
end
end
defp compute_visible_fields(dsl, show_fields, hide_fields) do
base =
if show_fields do
MapSet.new(show_fields)
else
dsl |> Ash.Resource.Info.public_fields() |> MapSet.new(& &1.name)
end
hidden = MapSet.new(hide_fields || [])
MapSet.difference(base, hidden)
end
defp check_invisible_fields(warnings, dsl, resource, visible, option, option_name) do
value = get_option(dsl, option)
if is_list(value) do
field_names = extract_field_names(value)
Enum.reduce(field_names, warnings, fn field, acc ->
if MapSet.member?(visible, field) do
acc
else
[invisible_field_warning(resource, field, option_name) | acc]
end
end)
else
warnings
end
end
defp check_invisible_field_names(warnings, dsl, resource, visible) do
field_names = AshGraphql.Resource.Info.field_names(dsl)
if is_list(field_names) and field_names != [] do
Enum.reduce(field_names, warnings, fn {field, _renamed}, acc ->
if MapSet.member?(visible, field) do
acc
else
[invisible_field_warning(resource, field, "field_names") | acc]
end
end)
else
warnings
end
end
defp check_invisible_relationships(warnings, dsl, resource, visible) do
# relationships option defaults to nil (meaning all), only check when explicitly set
relationships = Spark.Dsl.Extension.get_opt(dsl, [:graphql], :relationships, nil)
if is_list(relationships) do
Enum.reduce(relationships, warnings, fn rel, acc ->
if MapSet.member?(visible, rel) do
acc
else
[invisible_field_warning(resource, rel, "relationships") | acc]
end
end)
else
warnings
end
end
defp check_invisible_paginate_relationship_with(warnings, dsl, resource, visible) do
paginate_with = AshGraphql.Resource.Info.paginate_relationship_with(dsl)
if is_list(paginate_with) and paginate_with != [] do
Enum.reduce(paginate_with, warnings, fn {rel, _strategy}, acc ->
if MapSet.member?(visible, rel) do
acc
else
[invisible_field_warning(resource, rel, "paginate_relationship_with") | acc]
end
end)
else
warnings
end
end
defp check_invisible_attribute_input_types(warnings, dsl, resource, visible) do
input_types = AshGraphql.Resource.Info.attribute_input_types(dsl)
if is_list(input_types) and input_types != [] do
# Only check keys that are actual public attributes. Non-attribute keys
# (e.g. relationships) are caught by VerifyFieldReferences as invalid.
attribute_names =
dsl |> Ash.Resource.Info.public_attributes() |> MapSet.new(& &1.name)
Enum.reduce(input_types, warnings, fn {attr, _type}, acc ->
if not MapSet.member?(attribute_names, attr) or MapSet.member?(visible, attr) do
acc
else
[invisible_field_warning(resource, attr, "attribute_input_types") | acc]
end
end)
else
warnings
end
end
defp check_excluded_relationship(
warnings,
dsl,
resource,
included_rels,
:paginate_relationship_with
) do
paginate_with = AshGraphql.Resource.Info.paginate_relationship_with(dsl)
if is_list(paginate_with) and paginate_with != [] do
Enum.reduce(paginate_with, warnings, fn {rel, _strategy}, acc ->
if MapSet.member?(included_rels, rel) do
acc
else
[excluded_relationship_warning(resource, rel, "paginate_relationship_with") | acc]
end
end)
else
warnings
end
end
defp check_excluded_relationship(warnings, dsl, resource, included_rels, option) do
relationship_names =
dsl
|> Ash.Resource.Info.public_relationships()
|> MapSet.new(& &1.name)
values = get_relationship_option(dsl, option)
if is_list(values) and values != [] do
values
|> extract_field_names()
|> Enum.filter(&MapSet.member?(relationship_names, &1))
|> Enum.reduce(warnings, fn rel, acc ->
if MapSet.member?(included_rels, rel) do
acc
else
[excluded_relationship_warning(resource, rel, to_string(option)) | acc]
end
end)
else
warnings
end
end
defp excluded_relationship_warning(resource, field, option_name) do
"Relationship `#{inspect(field)}` in `#{option_name}` is not included in `relationships` " <>
"and will have no effect in #{inspect(resource)}."
end
defp get_relationship_option(dsl, :filterable_fields),
do: AshGraphql.Resource.Info.filterable_fields(dsl)
defp get_relationship_option(dsl, :field_names),
do: AshGraphql.Resource.Info.field_names(dsl)
defp get_relationship_option(dsl, :nullable_fields),
do: AshGraphql.Resource.Info.nullable_fields(dsl)
defp extract_field_names(fields) do
Enum.map(fields, fn
field when is_atom(field) -> field
{field, _value} when is_atom(field) -> field
end)
end
defp invisible_field_warning(resource, field, option_name) do
"Field `#{inspect(field)}` in `#{option_name}` is not visible " <>
"(it is hidden or not in show_fields) and will have no effect in #{inspect(resource)}."
end
defp get_option(dsl, :sortable_fields), do: AshGraphql.Resource.Info.sortable_fields(dsl)
defp get_option(dsl, :filterable_fields), do: AshGraphql.Resource.Info.filterable_fields(dsl)
defp get_option(dsl, :nullable_fields), do: AshGraphql.Resource.Info.nullable_fields(dsl)
end