fields_on_correct_type.ex (7631B)
1 defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do 2 @moduledoc false 3 4 # Validates document to ensure that all fields are provided on the correct type. 5 6 alias Absinthe.{Blueprint, Phase, Phase.Document.Validation.Utils, Schema, Type} 7 8 use Absinthe.Phase 9 10 @doc """ 11 Run the validation. 12 """ 13 @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() 14 def run(input, _options \\ []) do 15 result = Blueprint.prewalk(input, &handle_node(&1, input)) 16 {:ok, result} 17 end 18 19 @spec handle_node(Blueprint.node_t(), Schema.t()) :: Blueprint.node_t() 20 defp handle_node(%Blueprint.Document.Operation{schema_node: nil} = node, _) do 21 error = %Phase.Error{ 22 phase: __MODULE__, 23 message: "Operation \"#{node.type}\" not supported", 24 locations: [node.source_location] 25 } 26 27 node 28 |> flag_invalid(:unknown_operation) 29 |> put_error(error) 30 end 31 32 defp handle_node( 33 %{selections: selections, schema_node: parent_schema_node} = node, 34 %{schema: schema} = input 35 ) 36 when not is_nil(parent_schema_node) do 37 possible_parent_types = possible_types(parent_schema_node, schema) 38 39 selections = 40 Enum.map(selections, fn 41 %Blueprint.Document.Field{schema_node: nil} = field -> 42 type = named_type(parent_schema_node, schema) 43 44 field 45 |> flag_invalid(:unknown_field) 46 |> put_error( 47 error( 48 field, 49 type.name, 50 suggested_type_names(field.name, type, input), 51 suggested_field_names(field.name, type, input) 52 ) 53 ) 54 55 %Blueprint.Document.Fragment.Spread{errors: []} = spread -> 56 fragment = Enum.find(input.fragments, &(&1.name == spread.name)) 57 possible_child_types = possible_types(fragment.schema_node, schema) 58 59 if Enum.any?(possible_child_types, &(&1 in possible_parent_types)) do 60 spread 61 else 62 spread_error(spread, possible_parent_types, possible_child_types, schema) 63 end 64 65 %Blueprint.Document.Fragment.Inline{} = fragment -> 66 possible_child_types = possible_types(fragment.schema_node, schema) 67 68 if Enum.any?(possible_child_types, &(&1 in possible_parent_types)) do 69 fragment 70 else 71 spread_error(fragment, possible_parent_types, possible_child_types, schema) 72 end 73 74 other -> 75 other 76 end) 77 78 %{node | selections: selections} 79 end 80 81 defp handle_node(node, _) do 82 node 83 end 84 85 defp idents_to_names(idents, schema) do 86 for ident <- idents do 87 Absinthe.Schema.lookup_type(schema, ident).name 88 end 89 end 90 91 defp spread_error(spread, parent_types_idents, child_types_idents, schema) do 92 parent_types = idents_to_names(parent_types_idents, schema) 93 child_types = idents_to_names(child_types_idents, schema) 94 95 msg = """ 96 Fragment spread has no type overlap with parent. 97 Parent possible types: #{inspect(parent_types)} 98 Spread possible types: #{inspect(child_types)} 99 """ 100 101 error = %Phase.Error{ 102 phase: __MODULE__, 103 message: msg, 104 locations: [spread.source_location] 105 } 106 107 spread 108 |> flag_invalid(:invalid_spread) 109 |> put_error(error) 110 end 111 112 defp possible_types(%{type: type}, schema) do 113 possible_types(type, schema) 114 end 115 116 defp possible_types(type, schema) do 117 schema 118 |> Absinthe.Schema.lookup_type(type) 119 |> case do 120 %Type.Object{identifier: identifier} -> 121 [identifier] 122 123 %Type.Interface{identifier: identifier} -> 124 schema.__absinthe_interface_implementors__ 125 |> Map.fetch!(identifier) 126 127 %Type.Union{types: types} -> 128 types 129 130 _ -> 131 [] 132 end 133 end 134 135 @spec named_type(Type.t(), Schema.t()) :: Type.named_t() 136 defp named_type(%Type.Field{} = node, schema) do 137 Schema.lookup_type(schema, node.type) 138 end 139 140 defp named_type(%{name: _} = node, _) do 141 node 142 end 143 144 # Generate the error for a field 145 @spec error(Blueprint.node_t(), String.t(), [String.t()], [String.t()]) :: Phase.Error.t() 146 defp error(field_node, parent_type_name, type_suggestions, field_suggestions) do 147 %Phase.Error{ 148 phase: __MODULE__, 149 message: 150 error_message(field_node.name, parent_type_name, type_suggestions, field_suggestions), 151 locations: [field_node.source_location] 152 } 153 end 154 155 @doc """ 156 Generate an error for a field 157 """ 158 @spec error_message(String.t(), String.t(), [String.t()], [String.t()]) :: String.t() 159 def error_message(field_name, type_name, type_suggestions \\ [], field_suggestions \\ []) 160 161 def error_message(field_name, type_name, [], []) do 162 ~s(Cannot query field "#{field_name}" on type "#{type_name}".) 163 end 164 165 def error_message(field_name, type_name, [], field_suggestions) do 166 error_message(field_name, type_name) <> 167 Utils.MessageSuggestions.suggest_message(field_suggestions) 168 end 169 170 def error_message(field_name, type_name, type_suggestions, []) do 171 error_message(field_name, type_name) <> 172 Utils.MessageSuggestions.suggest_fragment_message(type_suggestions) 173 end 174 175 def error_message(field_name, type_name, type_suggestions, _) do 176 error_message(field_name, type_name, type_suggestions) 177 end 178 179 defp suggested_type_names(external_field_name, type, blueprint) do 180 internal_field_name = 181 case blueprint.adapter.to_internal_name(external_field_name, :field) do 182 nil -> external_field_name 183 internal_field_name -> internal_field_name 184 end 185 186 possible_types = find_possible_types(internal_field_name, type, blueprint.schema) 187 188 possible_interfaces = 189 find_possible_interfaces(internal_field_name, possible_types, blueprint.schema) 190 191 possible_interfaces 192 |> Enum.map(& &1.name) 193 |> Enum.concat(Enum.map(possible_types, & &1.name)) 194 |> Enum.sort() 195 end 196 197 defp suggested_field_names(external_field_name, %{fields: _} = type, blueprint) do 198 internal_field_name = 199 case blueprint.adapter.to_internal_name(external_field_name, :field) do 200 nil -> external_field_name 201 internal_field_name -> internal_field_name 202 end 203 204 Map.values(type.fields) 205 |> Enum.map(& &1.name) 206 |> Absinthe.Utils.Suggestion.sort_list(internal_field_name) 207 |> Enum.map(&blueprint.adapter.to_external_name(&1, :field)) 208 |> Enum.sort() 209 end 210 211 defp suggested_field_names(_, _, _) do 212 [] 213 end 214 215 defp find_possible_interfaces(field_name, possible_types, schema) do 216 possible_types 217 |> types_to_interface_idents 218 |> Enum.uniq() 219 |> sort_by_implementation_count(possible_types) 220 |> Enum.map(&Schema.lookup_type(schema, &1)) 221 |> types_with_field(field_name) 222 end 223 224 defp sort_by_implementation_count(iface_idents, types) do 225 Enum.sort_by(iface_idents, fn iface -> 226 count = 227 Enum.count(types, fn 228 %{interfaces: ifaces} -> 229 Enum.member?(ifaces, iface) 230 231 _ -> 232 false 233 end) 234 235 count 236 end) 237 |> Enum.reverse() 238 end 239 240 defp types_to_interface_idents(types) do 241 Enum.flat_map(types, fn 242 %{interfaces: ifaces} -> 243 ifaces 244 245 _ -> 246 [] 247 end) 248 end 249 250 defp find_possible_types(field_name, type, schema) do 251 schema 252 |> Schema.concrete_types(Type.unwrap(type)) 253 |> types_with_field(field_name) 254 end 255 256 defp types_with_field(types, field_name) do 257 Enum.filter(types, &type_with_field?(&1, field_name)) 258 end 259 260 defp type_with_field?(%{fields: fields}, field_name) do 261 Map.values(fields) 262 |> Enum.find(&(&1.name == field_name)) 263 end 264 265 defp type_with_field?(_, _) do 266 false 267 end 268 end