Applying Middleware

When it comes to applying middleware, we’ve got two different approaches available to us. Sometimes you have middleware that you want to apply to very specific fields, or even just one field. A logout mutation, for example, might use middleware to mutate the context, removing the current user.

Other times, you want to ensure that a particular middleware is always applied to every field in a certain object, or every field that has a particular name and return type. This is critical for something like authorization, where you want to protect against a programmer forgetting to specify that a field should be secured.

Absinthe provides two main approaches to handle these types of scenarios, which we’ll examine in turn.

Macro Approach

When you have specific fields on which you want to place middleware, you’ll want to reach for the middleware/2 macro. As we hinted at earlier, you’ve already been using this macro indirectly via the resolve/1 macro. To recap how it works:

 defmacro​ resolve(function_ast) ​do
 quote​ ​do
  middleware Absinthe.Resolution, ​unquote​(function_ast)
 end
 end

All we’ve been doing with resolve is placing a single piece of middleware on our field, Absinthe.Resolution, and giving it the function we want to execute. With this piece of knowledge, we’re ready to place our newly minted ChangesetErrors middleware on the :create_menu_item field.

 alias PlateSlateWeb.Schema.Middleware
 # Other schema content
 mutation ​do
 # Other mutation fields
  field ​:create_menu_item​, ​:menu_item_result​ ​do
  arg ​:input​, non_null(​:menu_item_input​)
  resolve &Resolvers.Menu.create_item/3
» middleware Middleware.ChangesetErrors
 end
 
 end

Notice how we’ve placed it after the resolve/1 call. When it comes time to execute the :create_menu_item field, Absinthe goes through each piece of middleware in order. We want our ChangesetErrors code to process errors that happen during resolution, so we need to place it after the resolve call. If we had it prior, there would never be any errors to transform yet!

You can have as many middleware/1,2 calls on a field as you like, and a few different varieties are supported. In addition to the module-based calls you’ve seen, you can also do inline functions, refer to specific remote functions, or even refer to local functions. You can also provide a configuration value that will be passed as the second argument during all middleware call/2 invocations.

With this logic extracted into middleware now, we can drastically simplify our :create_menu_item resolver:

 def​ create_item(_, %{​input:​ params}, _) ​do
 with​ {​:ok​, item} <- Menu.create_item(params) ​do
  {​:ok​, %{​menu_item:​ item}}
 end
 end

No longer do we need to worry about the error case at all, much less worry about transforming errors.

Running the tests confirms that we are still getting the right results back, even in the error case.

 $mix test test/plate_slate_web/schema/mutation/create_menu_item_test.exs
 ..
 
 Finished in 0.2 seconds
 2 tests, 0 failures

We’ve taught Absinthe how to handle changesets!

Callback Approach

As we look at our schema as a whole at this point, we can clearly see some other places where we want this error handling to happen. In fact, every field in our mutation object so far can return Ecto changeset errors, and those resolvers would be a lot cleaner if they could use this middleware instead. If we took the macro-based approach we have covered so far, that might look like this:

 mutation ​do
 
  field ​:ready_order​, ​:order_result​ ​do
  arg ​:id​, non_null(​:id​)
  resolve &Resolvers.Ordering.ready_order/3
» middleware Middleware.ChangesetErrors
 end
  field ​:complete_order​, ​:order_result​ ​do
  arg ​:id​, non_null(​:id​)
  resolve &Resolvers.Ordering.complete_order/3
» middleware Middleware.ChangesetErrors
 end
 
  field ​:place_order​, ​:order_result​ ​do
  arg ​:input​, non_null(​:place_order_input​)
  resolve &Resolvers.Ordering.place_order/3
» middleware Middleware.ChangesetErrors
 end
 
  field ​:create_menu_item​, ​:menu_item_result​ ​do
  arg ​:input​, non_null(​:menu_item_input​)
  resolve &Resolvers.Menu.create_item/3
» middleware Middleware.ChangesetErrors
 end
 end

Not only does this seem like unnecessary duplication of code, it’s also a bit of an error-prone approach because as your schema grows, it could become easy to miss a field. Even if you have tests to catch it (everyone always tests the error case, right?), what you really want is a schema-wide rule that says something to the effect of, “All fields on the mutation object should run this middleware after resolution.” Fortunately, we have a way to do just that and more. Meet the middleware/3 callback:

 defmodule​ PlateSlateWeb.Schema ​do
 use​ Absinthe.Schema
 
  alias PlateSlateWeb.Resolvers
  alias PlateSlateWeb.Schema.Middleware
 
 def​ middleware(middleware, _field, _object) ​do
  middleware
 end

When you use Absinthe.Schema in your schema module, it injects a middleware/3 function that looks just like the previous one, which you can override if you want to do some dynamic logic. This function is called for every field in the schema, passing the list of middleware already configured for the field—set using the resolve/1 macro or a middleware/1,2 macro call elsewhere in the schema—as well as the actual field and object structs themselves.

We took a look at these %Absinthe.Type.Field{} and %Absinthe.Type.Object{} structs back at the very beginning, in Chapter 2, Building a Schema. If you remember, they both have an :identifier key that’s set when the field and object are defined in our schema. Let’s take a closer look at what’s going on here. We’ll modify the function and insert a bit of inspection code, and then run a simple query against our API:

 def​ middleware(middleware, field, object) ​do
  IO.inspect [
 object:​ object.identifier,
 field:​ field.identifier,
  ]
  middleware
 end

If we compile our program with iex -S mix and then run this:

 iex(1)>​ Absinthe.run(​"​​"​​"
 { search(matching: "Reuben") { name } }
 """, PlateSlateWeb.Schema)
 [object: :query, field: :menu_items]
 [object: :query, field: :search]
 [object: :menu_item, field: :added_on]
 [object: :menu_item, field: :description]
 [object: :menu_item, field: :id]
 [object: :menu_item, field: :name]
 [object: :menu_item, field: :price]
 {:ok, %{data: %{"search" => [%{"name" => "Reuben"}]}}}

We get a lot of output! You also probably got even more output at compile time. What’s going on here? If you run the query again, there’s no output at all. What’s going on?

The middleware/3 callback is run on every field for an object whenever that object is loaded from the schema. The compile-time output happens because Absinthe does a lot of compile-time checking of your schema, which involves loading every object out of it to make sure it’s valid.

Then when we run the GraphQL query, Absinthe has to load the root query object and the menu item object out of the schema in order to execute the document. What about the lack of output if you run the query again? In the current version of Absinthe, there is some in-memory caching that happens on loaded schema objects. If you run the same query twice, it’s just going to re-use the in-memory cache for the second run, so no loading happens. In that case, the middleware/3 callback doesn’t need to be run.

Back to our goal: we want to apply error-handling middleware on the mutation object, but not elsewhere. With middleware/3, this becomes nice and easy!

 def​ middleware(middleware, _field, %{​identifier:​ ​:mutation​}) ​do
  middleware ++ [Middleware.ChangesetErrors]
 end
 def​ middleware(middleware, _field, _object) ​do
  middleware
 end

We’ve got two middleware/3 clauses: one that pattern matches for an object with the identifier :mutation, and another that is just a fallback. In the :mutation clause, we’re taking whatever existing middleware is already specified on the field like a resolver, and we’re appending our ChangesetErrors module to the end.

Much like when we had a sequence of middleware/1,2 calls in our schema earlier in the chapter, the middleware placed in this list is executed in order. Remove the middleware callback :create_menu_item field and re-run the tests, confirming that our changeset handling is being applied correctly by our callback.

 $mix test test/plate_slate_web/schema/mutation/create_menu_item_test.exs
 ..
 
 Finished in 0.2 seconds
 2 tests, 0 failures

We can also significantly improve the ordering resolver as well now. Here’s just one particular resolver function by way of example:

 def​ ready_order(_, %{​id:​ id}, _) ​do
  order = Ordering.get_order!(id)
 with​ {​:ok​, order} <- Ordering.update_order(order, %{​state:​ ​"​​ready"​}) ​do
  {​:ok​, %{​order:​ order}}
 end
 end

The middleware/3 callback is incredibly powerful. At the end of the day, all we had to do was add a single function and a few lines of code to our schema in order to educate Absinthe about how to handle changesets coming back from our mutation resolvers, which gave us big wins in readability.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.70.185