resolution.ex (11230B)
1 defmodule Absinthe.Resolution do 2 @moduledoc """ 3 Information about the current resolution. It is created by adding field specific 4 information to the more general `%Absinthe.Blueprint.Execution{}` struct. 5 6 In many ways like the `%Conn{}` from `Plug`, the `%Absinthe.Resolution{}` is the 7 piece of information that passed along from middleware to middleware as part of 8 resolution. 9 10 ## Contents 11 - `:adapter` - The adapter used for any name conversions. 12 - `:definition` - The Blueprint definition for this field. 13 - `:context` - The context passed to `Absinthe.run`. 14 - `:root_value` - The root value passed to `Absinthe.run`, if any. 15 - `:parent_type` - The parent type for the field. 16 - `:private` - Operates similarly to the `:private` key on a `%Plug.Conn{}` 17 and is a place for libraries (and similar) to store their information. 18 - `:schema` - The current schema. 19 - `:source` - The resolved parent object; source of this field. 20 21 When a `%Resolution{}` is accessed via middleware, you may want to update the 22 context (e.g. to cache a dataloader instance or the result of an ecto query). 23 Updating the context can be done simply by using the map updating syntax (or 24 `Map.put/3`): 25 26 ```elixir 27 %{resolution | context: new_context} 28 # OR 29 Map.put(resolution, :context, new_context) 30 ``` 31 32 To access the schema type for this field, see the `definition.schema_node`. 33 """ 34 35 @typedoc """ 36 The arguments that are passed from the schema. (e.g. id of the record to be 37 fetched) 38 """ 39 @type arguments :: %{optional(atom) => any} 40 @type source :: any 41 42 @type t :: %__MODULE__{ 43 value: term, 44 errors: [term], 45 adapter: Absinthe.Adapter.t(), 46 context: map, 47 root_value: any, 48 schema: Absinthe.Schema.t(), 49 definition: Absinthe.Blueprint.node_t(), 50 parent_type: Absinthe.Type.t(), 51 source: source, 52 state: field_state, 53 acc: %{any => any}, 54 extensions: %{any => any}, 55 arguments: arguments, 56 fragments: [Absinthe.Blueprint.Document.Fragment.Named.t()] 57 } 58 59 defstruct [ 60 :value, 61 :adapter, 62 :context, 63 :parent_type, 64 :root_value, 65 :definition, 66 :schema, 67 :source, 68 errors: [], 69 middleware: [], 70 acc: %{}, 71 arguments: %{}, 72 extensions: %{}, 73 private: %{}, 74 path: [], 75 state: :unresolved, 76 fragments: [], 77 fields_cache: %{} 78 ] 79 80 def resolver_spec(fun) do 81 {{__MODULE__, :call}, fun} 82 end 83 84 @type field_state :: :unresolved | :resolved | :suspended 85 86 @doc """ 87 Get the child fields under the current field. 88 89 See `project/2` for details. 90 """ 91 def project(info) do 92 case info.definition.schema_node.type do 93 %Absinthe.Type.Interface{} -> 94 raise need_concrete_type_error() 95 96 %Absinthe.Type.Union{} -> 97 raise need_concrete_type_error() 98 99 schema_node -> 100 project(info, schema_node) 101 end 102 end 103 104 @doc """ 105 Get the current path. 106 107 Each `Absinthe.Resolution` struct holds the current result path as a list of 108 blueprint nodes and indices. Usually however you don't need the full AST list 109 and instead just want the path that will eventually end up in the result. 110 111 For that, use this function. 112 113 ## Examples 114 Given some query: 115 ```graphql 116 {users { email }} 117 ``` 118 119 If you called this function inside a resolver on the users email field it 120 returns a value like: 121 122 ```elixir 123 resolve fn _, _, resolution -> 124 Absinthe.Resolution.path(resolution) #=> ["users", 5, "email"] 125 end 126 ``` 127 128 In this case `5` is the 0 based index in the list of users the field is currently 129 at. 130 """ 131 def path(%{path: path}) do 132 path 133 |> Enum.reverse() 134 |> Enum.drop(1) 135 |> Enum.map(&field_name/1) 136 end 137 138 defp field_name(%{alias: nil, name: name}), do: name 139 defp field_name(%{alias: name}), do: name 140 defp field_name(%{name: name}), do: name 141 defp field_name(index), do: index 142 143 @doc """ 144 Get the child fields under the current field. 145 146 ## Example 147 148 Given a document like: 149 ```graphql 150 { user { id name }} 151 ``` 152 153 ``` 154 field :user, :user do 155 resolve fn _, info -> 156 child_fields = Absinthe.Resolution.project(info) |> Enum.map(&(&1.name)) 157 # ... 158 end 159 end 160 ``` 161 162 `child_fields` will be `["id", "name"]`. 163 164 It correctly handles fragments, so for example if you had the document: 165 ```graphql 166 { 167 user { 168 ... on User { 169 id 170 } 171 ... on Named { 172 name 173 } 174 } 175 } 176 ``` 177 178 you would still get a nice and simple `child_fields` that was `["id", "name"]`. 179 """ 180 def project( 181 %{ 182 definition: %{selections: selections}, 183 path: path, 184 fields_cache: cache 185 } = info, 186 type 187 ) do 188 type = Absinthe.Schema.lookup_type(info.schema, type) 189 190 {fields, _} = Absinthe.Resolution.Projector.project(selections, type, path, cache, info) 191 192 fields 193 end 194 195 defp need_concrete_type_error() do 196 """ 197 You tried to project from a field that is an abstract type without concrete type information! 198 Use `project/2` instead of `project/1`, and supply the type yourself please! 199 """ 200 end 201 202 def call(%{state: :unresolved} = res, resolution_function) do 203 result = 204 case resolution_function do 205 fun when is_function(fun, 2) -> 206 fun.(res.arguments, res) 207 208 fun when is_function(fun, 3) -> 209 fun.(res.source, res.arguments, res) 210 211 {mod, fun} -> 212 apply(mod, fun, [res.source, res.arguments, res]) 213 214 _ -> 215 raise Absinthe.ExecutionError, """ 216 Field resolve property must be a 2 arity anonymous function, 3 arity 217 anonymous function, or a `{Module, :function}` tuple. 218 219 Instead got: #{inspect(resolution_function)} 220 221 Resolving field: 222 223 #{res.definition.name} 224 225 Defined at: 226 227 #{res.definition.schema_node.__reference__.location.file}:#{ 228 res.definition.schema_node.__reference__.location.line 229 } 230 231 Info: #{inspect(res)} 232 """ 233 end 234 235 put_result(res, result) 236 end 237 238 def call(res, _), do: res 239 240 def path_string(%__MODULE__{path: path}) do 241 Enum.map(path, fn 242 %{name: name, alias: alias} -> 243 alias || name 244 245 %{schema_node: schema_node} -> 246 schema_node.name 247 end) 248 end 249 250 @doc """ 251 Handy function for applying user function result tuples to a resolution struct 252 253 User facing functions generally return one of several tuples like `{:ok, val}` 254 or `{:error, reason}`. This function handles applying those various tuples 255 to the resolution struct. 256 257 The resolution state is updated depending on the tuple returned. `:ok` and 258 `:error` tuples set the state to `:resolved`, whereas middleware tuples set it 259 to `:unresolved`. 260 261 This is useful for middleware that wants to handle user facing functions, but 262 does not want to duplicate this logic. 263 """ 264 def put_result(res, {:ok, value}) do 265 %{res | state: :resolved, value: value} 266 end 267 268 def put_result(res, {:error, [{_, _} | _] = error_keyword}) do 269 %{res | state: :resolved, errors: [error_keyword]} 270 end 271 272 def put_result(res, {:error, errors}) do 273 %{res | state: :resolved, errors: List.wrap(errors)} 274 end 275 276 def put_result(res, {:plugin, module, opts}) do 277 put_result(res, {:middleware, module, opts}) 278 end 279 280 def put_result(res, {:middleware, module, opts}) do 281 %{res | state: :unresolved, middleware: [{module, opts} | res.middleware]} 282 end 283 284 def put_result(res, result) do 285 raise result_error(result, res.definition, res.source) 286 end 287 288 @doc false 289 def result_error({:error, _} = value, field, source) do 290 result_error( 291 value, 292 field, 293 source, 294 "You're returning an :error tuple, but did you forget to include a `:message`\nkey in every custom error (map or keyword list)?" 295 ) 296 end 297 298 def result_error(value, field, source) do 299 result_error( 300 value, 301 field, 302 source, 303 "Did you forget to return a valid `{:ok, any}` | `{:error, error_value}` tuple?" 304 ) 305 end 306 307 @doc """ 308 TODO: Deprecate 309 """ 310 def call(resolution_function, parent, args, field_info) do 311 case resolution_function do 312 fun when is_function(fun, 2) -> 313 fun.(args, field_info) 314 315 fun when is_function(fun, 3) -> 316 fun.(parent, args, field_info) 317 318 {mod, fun} -> 319 apply(mod, fun, [parent, args, field_info]) 320 321 _ -> 322 raise Absinthe.ExecutionError, """ 323 Field resolve property must be a 2 arity anonymous function, 3 arity 324 anonymous function, or a `{Module, :function}` tuple. 325 Instead got: #{inspect(resolution_function)} 326 Info: #{inspect(field_info)} 327 """ 328 end 329 end 330 331 def call(function, args, info) do 332 call(function, info.source, args, info) 333 end 334 335 @error_detail """ 336 ## For a data result 337 338 `{:ok, any}` result will do. 339 340 ### Examples: 341 342 A simple integer result: 343 344 {:ok, 1} 345 346 Something more complex: 347 348 {:ok, %Model.Thing{some: %{complex: :data}}} 349 350 ## For an error result 351 352 One or more errors for a field can be returned in a single `{:error, error_value}` tuple. 353 354 `error_value` can be: 355 - A simple error message string. 356 - A map containing `:message` key, plus any additional serializable metadata. 357 - A keyword list containing a `:message` key, plus any additional serializable metadata. 358 - A list containing multiple of any/all of these. 359 - Any other value compatible with `to_string/1`. 360 361 ### Examples 362 363 A simple error message: 364 365 {:error, "Something bad happened"} 366 367 Multiple error messages: 368 369 {:error, ["Something bad", "Even worse"] 370 371 Single custom errors (note the required `:message` keys): 372 373 {:error, message: "Unknown user", code: 21} 374 {:error, %{message: "A database error occurred", details: format_db_error(some_value)}} 375 376 Three errors of mixed types: 377 378 {:error, ["Simple message", [message: "A keyword list error", code: 1], %{message: "A map error"}]} 379 380 Generic handler for interoperability with errors from other libraries: 381 382 {:error, :foo} 383 {:error, 1.0} 384 {:error, 2} 385 386 ## To activate a plugin 387 388 `{:plugin, NameOfPluginModule, term}` to activate a plugin. 389 390 See `Absinthe.Resolution.Plugin` for more information. 391 392 """ 393 def result_error(value, field, source, guess) do 394 Absinthe.ExecutionError.exception(""" 395 Invalid value returned from resolver. 396 397 Resolving field: 398 399 #{field.name} 400 401 Defined at: 402 403 #{field.schema_node.__reference__.location.file}:#{ 404 field.schema_node.__reference__.location.line 405 } 406 407 Resolving on: 408 409 #{inspect(source)} 410 411 Got value: 412 413 #{inspect(value)} 414 415 ... 416 417 #{guess} 418 419 ... 420 421 The result must be one of the following... 422 423 #{@error_detail} 424 """) 425 end 426 end 427 428 defimpl Inspect, for: Absinthe.Resolution do 429 import Inspect.Algebra 430 431 def inspect(res, opts) do 432 # TODO: better inspect representation 433 inner = 434 res 435 |> Map.from_struct() 436 |> Map.update!(:fields_cache, fn _ -> 437 "#fieldscache<...>" 438 end) 439 |> Map.to_list() 440 |> Inspect.List.inspect(opts) 441 442 concat(["#Absinthe.Resolution<", inner, ">"]) 443 end 444 end