middleware.ex (10922B)
1 defmodule Absinthe.Middleware do 2 @moduledoc """ 3 Middleware enables custom resolution behaviour on a field. 4 5 All resolution happens through middleware. Even `resolve` functions are 6 middleware, as the `resolve` macro is just 7 8 ``` 9 quote do 10 middleware Absinthe.Resolution, unquote(function_ast) 11 end 12 ``` 13 14 Resolution happens by reducing a list of middleware spec onto an 15 `%Absinthe.Resolution{}` struct. 16 17 ## Example 18 19 ``` 20 defmodule MyApp.Web.Authentication do 21 @behaviour Absinthe.Middleware 22 23 def call(resolution, _config) do 24 case resolution.context do 25 %{current_user: _} -> 26 resolution 27 _ -> 28 resolution 29 |> Absinthe.Resolution.put_result({:error, "unauthenticated"}) 30 end 31 end 32 end 33 ``` 34 35 By specifying `@behaviour Absinthe.Middleware` the compiler will ensure that 36 we provide a `def call` callback. This function takes an 37 `%Absinthe.Resolution{}` struct and will also need to return one such struct. 38 39 On that struct there is a `context` key which holds the absinthe context. This 40 is generally where things like the current user are placed. For more 41 information on how the current user ends up in the context please see our full 42 authentication guide on the website. 43 44 Our `call/2` function simply checks the context to see if there is a current 45 user. If there is, we pass the resolution onward. If there is not, we update 46 the resolution state to `:resolved` and place an error result. 47 48 Middleware can be placed on a field in three different ways: 49 50 1. Using the `Absinthe.Schema.Notation.middleware/2` 51 macro used inside a field definition. 52 2. Using the `middleware/3` callback in your schema. 53 3. Returning a `{:middleware, middleware_spec, config}` 54 tuple from a resolution function. 55 56 ## The `middleware/2` macro 57 58 For placing middleware on a particular field, it's handy to use 59 the `middleware/2` macro. 60 61 Middleware will be run in the order in which they are specified. 62 The `middleware/3` callback has final say on what middleware get 63 set. 64 65 Examples 66 67 `MyApp.Web.Authentication` would run before resolution, and `HandleError` would run after. 68 ``` 69 field :hello, :string do 70 middleware MyApp.Web.Authentication 71 resolve &get_the_string/2 72 middleware HandleError, :foo 73 end 74 ``` 75 76 Anonymous functions are a valid middleware spec. A nice use case 77 is altering the context in a logout mutation. Mutations are the 78 only time the context should be altered. This is not enforced. 79 ``` 80 field :logout, :query do 81 middleware fn res, _ -> 82 %{res | 83 context: Map.delete(res.context, :current_user), 84 value: "logged out", 85 state: :resolved 86 } 87 end 88 end 89 ``` 90 91 `middleware/2` even accepts local public function names. Note 92 that `middleware/2` is the only thing that can take local function 93 names without an associated module. If not using macros, use 94 `{{__MODULE__, :function_name}, []}` 95 ``` 96 def auth(res, _config) do 97 # auth logic here 98 end 99 100 query do 101 field :hello, :string do 102 middleware :auth 103 resolve &get_the_string/2 104 end 105 end 106 ``` 107 108 ## The `middleware/3` callback 109 110 `middleware/3` is a function callback on a schema. When you `use 111 Absinthe.Schema` a default implementation of this function is placed in your 112 schema. It is passed the existing middleware for a field, the field itself, 113 and the object that the field is a part of. 114 115 So for example if your schema contained: 116 117 ``` 118 object :user do 119 field :name, :string 120 field :age, :integer 121 end 122 123 query do 124 field :lookup_user, :user do 125 resolve fn _, _ -> 126 {:ok, %{name: "Bob"}} 127 end 128 end 129 end 130 131 def middleware(middleware, field, object) do 132 middleware |> IO.inspect 133 field |> IO.inspect 134 object |> IO.inspect 135 136 middleware 137 end 138 ``` 139 140 Given a document like: 141 ```graphql 142 { lookupUser { name }} 143 ``` 144 145 `object` is each object that is accessed while executing the document. In our 146 case that is the `:user` object and the `:query` object. `field` is every 147 field on that object, and middleware is a list of whatever middleware 148 spec have been configured by the schema on that field. Concretely 149 then, the function will be called , with the following arguments: 150 151 ``` 152 YourSchema.middleware([{Absinthe.Resolution, #Function<20.52032458/0>}], lookup_user_field_of_root_query_object, root_query_object) 153 YourSchema.middleware([{Absinthe.Middleware.MapGet, :name}], name_field_of_user, user_object) 154 YourSchema.middleware([{Absinthe.Middleware.MapGet, :age}], age_field_of_user, user_object) 155 ``` 156 157 In the latter two cases we see that the middleware list is empty. In the first 158 case we see one middleware spec, which is placed by the `resolve` macro used in the 159 `:lookup_user` field. 160 161 ### Default Middleware 162 163 One use of `middleware/3` is setting the default middleware on a field. 164 By default middleware is placed on a 165 field that looks up a field by its snake case identifier, ie `:resource_name`. 166 Here is an example of how to change the default to use a camel cased string, 167 IE, "resourceName". 168 169 ``` 170 def middleware(middleware, %{identifier: identifier} = field, object) do 171 camelized = 172 identifier 173 |> Atom.to_string 174 |> Macro.camelize 175 176 new_middleware_spec = {{__MODULE__, :get_camelized_key}, camelized} 177 178 Absinthe.Schema.replace_default(middleware, new_middleware_spec, field, object) 179 end 180 181 def get_camelized_key(%{source: source} = res, key) do 182 %{res | state: :resolved, value: Map.get(source, key)} 183 end 184 ``` 185 186 There's a lot going on here so let's unpack it. We need to define a 187 specification to tell Absinthe what middleware to run. The form we're using is 188 `{{MODULE, :function_to_call}, options_of_middleware}`. For our purposes we're 189 simply going to use a function in the schema module itself 190 `get_camelized_key`. 191 192 We then use the `Absinthe.Schema.replace_default/4` function to swap out the 193 default middleware already present in the middleware list with the new one we 194 want to use. It handles going through the existing list of middleware and 195 seeing if it's using the default or if it has custom resolvers on it. If it's 196 using the default, the function applies our newly defined middleware spec. 197 198 Like all middleware functions, `:get_camelized_key` takes a resolution struct, 199 and options. The options is the camelized key we generated. We get the 200 camelized string from the parent map, and set it as the value of the 201 resolution struct. Finally we mark the resolution state `:resolved`. 202 203 Side note: This `middleware/3` function is called whenever we pull the type 204 out of the schema. The middleware itself is run every time we get a field on 205 an object. If we have 1000 objects and we were doing the camelization logic 206 INSIDE the middleware, we would compute the camelized string 1000 times. By 207 doing it in the `def middleware` callback we do it just once. 208 209 ### Changes Since 1.3 210 211 In Absinthe 1.3, fields without any `middleware/2` or `resolve/1` calls would 212 show up with an empty list `[]` as its middleware in the `middleware/3` 213 function. If no middleware was applied in the function and it also returned `[]`, 214 THEN Absinthe would apply the default. 215 216 This made it very easy to accidentally break your schema if you weren't 217 particularly careful with your pattern matching. Now the defaults are applied 218 FIRST by absinthe, and THEN passed to `middleware/3`. Consequently, the 219 middleware list argument should always have at least one value. This is also 220 why there is now the `replace_default/4` function, because it handles telling 221 the difference between a field with a resolver and a field with the default. 222 223 ### Object Wide Authentication 224 225 Let's use our authentication middleware from earlier, and place it on every 226 field in the query object. 227 228 ``` 229 defmodule MyApp.Web.Schema do 230 use Absinthe.Schema 231 232 query do 233 field :private_field, :string do 234 resolve fn _, _ -> 235 {:ok, "this can only be viewed if authenticated"} 236 end 237 end 238 end 239 240 def middleware(middleware, _field, %Absinthe.Type.Object{identifier: identifier}) 241 when identifier in [:query, :subscription, :mutation] do 242 [MyApp.Web.Authentication | middleware] 243 end 244 def middleware(middleware, _field, _object) do 245 middleware 246 end 247 end 248 ``` 249 250 It is important to note that we are matching for the `:query`, `:subscription` 251 or `:mutation` identifier types. We do this because the middleware function 252 will be called for each field in the schema. If we didn't limit it to those 253 types, we would be applying authentication to every field in the entire 254 schema, even stuff like `:name` or `:age`. This generally isn't necessary 255 provided you authenticate at the entrypoints. 256 257 ## Main Points 258 259 - Middleware functions take a `%Absinthe.Resolution{}` struct, and return one. 260 - All middleware on a field are always run, make sure to pattern match on the 261 state if you care. 262 """ 263 264 @type function_name :: atom 265 266 @type spec :: 267 module 268 | {module, term} 269 | {{module, function_name}, term} 270 | (Absinthe.Resolution.t(), term -> Absinthe.Resolution.t()) 271 272 @doc """ 273 This is the main middleware callback. 274 275 It receives an `%Absinthe.Resolution{}` struct and it needs to return an 276 `%Absinthe.Resolution{}` struct. The second argument will be whatever value 277 was passed to the `middleware` call that setup the middleware. 278 """ 279 @callback call(Absinthe.Resolution.t(), term) :: Absinthe.Resolution.t() 280 281 @doc false 282 def shim(res, {object, field, middleware}) do 283 schema = res.schema 284 object = Absinthe.Schema.lookup_type(schema, object) 285 field = Map.fetch!(object.fields, field) 286 287 middleware = expand(schema, middleware, field, object) 288 289 %{res | middleware: middleware} 290 end 291 292 @doc "For testing and inspection purposes" 293 def unshim([{{__MODULE__, :shim}, {object, field, middleware}}], schema) do 294 object = Absinthe.Schema.lookup_type(schema, object) 295 field = Map.fetch!(object.fields, field) 296 expand(schema, middleware, field, object) 297 end 298 299 @doc false 300 def expand(schema, middleware, field, object) do 301 expanded = 302 middleware 303 |> Enum.flat_map(&get_functions/1) 304 |> Absinthe.Schema.Notation.__ensure_middleware__(field, object) 305 306 case middleware do 307 [{:ref, Absinthe.Phase.Schema.Introspection, _}] -> 308 expanded 309 310 [{:ref, Absinthe.Type.BuiltIns.Introspection, _}] -> 311 expanded 312 313 [{:ref, Absinthe.Phase.Schema.DeprecatedDirectiveFields, _}] -> 314 expanded 315 316 _ -> 317 schema.middleware(expanded, field, object) 318 end 319 end 320 321 defp get_functions({:ref, module, identifier}) do 322 module.__absinthe_function__(identifier, :middleware) 323 end 324 325 defp get_functions(val) do 326 List.wrap(val) 327 end 328 end