variable_types_match.ex (5476B)
1 defmodule Absinthe.Phase.Document.Arguments.VariableTypesMatch do 2 @moduledoc false 3 4 # Implements: 5.8.5. All Variable Usages are Allowed 5 # Specifically, it implements "Variable usages must be compatible with the arguments they are passed to." 6 # See relevant counter-example: https://spec.graphql.org/draft/#example-2028e 7 8 use Absinthe.Phase 9 10 alias Absinthe.Blueprint 11 alias Absinthe.Blueprint.Document.{Operation, Fragment} 12 alias Absinthe.Type 13 14 def run(blueprint, _) do 15 blueprint = 16 blueprint 17 |> check_operations() 18 |> check_fragments() 19 20 {:ok, blueprint} 21 end 22 23 def check_operations(%Blueprint{} = blueprint) do 24 blueprint 25 |> Map.update!(:operations, fn operations -> 26 Enum.map(operations, &check_variable_types/1) 27 end) 28 end 29 30 # A single fragment may be used by multiple operations. 31 # Each operation may define its own variables. 32 # This checks that each fragment is simultaneously consistent with the 33 # variables defined in each of the operations which use that fragment. 34 def check_fragments(%Blueprint{} = blueprint) do 35 blueprint 36 |> Map.update!(:fragments, fn fragments -> 37 fragments 38 |> Enum.map(fn fragment -> 39 blueprint.operations 40 |> Enum.filter(&Operation.uses?(&1, fragment)) 41 |> Enum.reduce(fragment, fn operation, fragment_acc -> 42 check_variable_types(operation, fragment_acc) 43 end) 44 end) 45 end) 46 end 47 48 def check_variable_types(%Operation{} = op) do 49 variable_defs = Map.new(op.variable_definitions, &{&1.name, &1}) 50 Blueprint.prewalk(op, &check_variable_type(&1, op.name, variable_defs)) 51 end 52 53 def check_variable_types(%Operation{} = op, %Fragment.Named{} = fragment) do 54 variable_defs = Map.new(op.variable_definitions, &{&1.name, &1}) 55 Blueprint.prewalk(fragment, &check_variable_type(&1, op.name, variable_defs)) 56 end 57 58 defp check_variable_type(%{schema_node: nil} = node, _, _) do 59 {:halt, node} 60 end 61 62 defp check_variable_type( 63 %Absinthe.Blueprint.Input.Argument{ 64 input_value: %Blueprint.Input.Value{ 65 raw: %{content: %Blueprint.Input.Variable{} = variable} 66 } 67 } = node, 68 operation_name, 69 variable_defs 70 ) do 71 location_type = node.input_value.schema_node 72 location_definition = node.schema_node 73 74 case Map.get(variable_defs, variable.name) do 75 %{schema_node: variable_type} = variable_definition -> 76 if types_compatible?( 77 variable_type, 78 location_type, 79 variable_definition, 80 location_definition 81 ) do 82 node 83 else 84 variable = 85 put_error( 86 variable, 87 error(operation_name, variable, variable_definition, location_type) 88 ) 89 90 {:halt, put_in(node.input_value.raw.content, variable)} 91 end 92 93 _ -> 94 node 95 end 96 end 97 98 defp check_variable_type(node, _, _) do 99 node 100 end 101 102 def types_compatible?(type, type, _, _) do 103 true 104 end 105 106 def types_compatible?( 107 %Type.NonNull{of_type: nullable_variable_type}, 108 location_type, 109 variable_definition, 110 location_definition 111 ) do 112 types_compatible?( 113 nullable_variable_type, 114 location_type, 115 variable_definition, 116 location_definition 117 ) 118 end 119 120 def types_compatible?( 121 %Type.List{of_type: item_variable_type}, 122 %Type.List{ 123 of_type: item_location_type 124 }, 125 variable_definition, 126 location_definition 127 ) do 128 types_compatible?( 129 item_variable_type, 130 item_location_type, 131 variable_definition, 132 location_definition 133 ) 134 end 135 136 # https://github.com/graphql/graphql-spec/blame/October2021/spec/Section%205%20--%20Validation.md#L1885-L1893 137 # if argument has default value the variable can be nullable 138 def types_compatible?(nullable_type, %Type.NonNull{of_type: nullable_type}, _, %{ 139 default_value: default_value 140 }) 141 when not is_nil(default_value) do 142 true 143 end 144 145 # https://github.com/graphql/graphql-spec/blame/main/spec/Section%205%20--%20Validation.md#L2000-L2005 146 # This behavior is explicitly supported for compatibility with earlier editions of this specification. 147 def types_compatible?( 148 nullable_type, 149 %Type.NonNull{of_type: nullable_type}, 150 %{ 151 default_value: value 152 }, 153 _ 154 ) 155 when is_struct(value) do 156 true 157 end 158 159 def types_compatible?(_, _, _, _) do 160 false 161 end 162 163 defp error(operation_name, variable, variable_definition, location_type) do 164 # need to rely on the type reference here, since the schema node may not be available 165 # as the type could not exist in the schema 166 variable_name = Absinthe.Blueprint.TypeReference.name(variable_definition.type) 167 168 %Absinthe.Phase.Error{ 169 phase: __MODULE__, 170 message: 171 error_message( 172 operation_name, 173 variable, 174 variable_name, 175 Absinthe.Type.name(location_type) 176 ), 177 locations: [variable.source_location] 178 } 179 end 180 181 def error_message(op, variable, variable_name, location_type) do 182 start = 183 case op || "" do 184 "" -> "Variable" 185 op -> "In operation `#{op}`, variable" 186 end 187 188 "#{start} `#{Blueprint.Input.inspect(variable)}` of type `#{variable_name}` found as input to argument of type `#{ 189 location_type 190 }`." 191 end 192 end