Setting Defaults

Now that we feel comfortable with def middleware, we’re in a good place to address an important question: how do fields without specific resolvers actually resolve anything? Throughout the entire book so far, we’ve had stuff in our schema like this:

 object ​:menu_item​ ​do
  field ​:name​, ​:string
 # ... other fields
 end

Despite the fact that the :name field has no obvious resolver, we nonetheless can include it in a GraphQL query and we get a result. What we need to do is look at what actually happens here because what you’ll find is not only a feature you can use yourself, but more importantly, a tool you can customize as necessary when the default behavior doesn’t suit the data you are working with.

As you might remember from Making a Query, what happens at this point is that the default resolution logic does something equivalent to this:

 field ​:name​, ​:string​ ​do
  resolve ​fn​ parent, _, _ ->
  {​:ok​, Map.get(parent, ​:name​)}
 end
 end

Any time a def middleware callback returns an empty list of middleware for a field, Absinthe adds the incredibly simple middleware spec [{Absinthe.Middleware.MapGet, field.identifier}]. Here it is in full:

 def​ call(%{​source:​ source} = resolution, key) ​do
  %{resolution | ​state:​ ​:resolved​, ​value:​ Map.get(source, key)}
 end

This is handy when the parent entity in question is a map with atom keys, but it isn’t what we want in every scenario.

It’s increasingly common that an API will expose data from a variety of data sources, only some of which may have a fully structured schema on hand that will give you nice maps with atom keys. Whether you’re hitting a NoSQL database or calling out to a third-party API for JSON data, you’re going to eventually run into a situation where the data that you want to expose via GraphQL has string keys or keys that aren’t quite what you want.

We can get some of this NoSQL experience without even changing databases, as PostgreSQL has significantly expanded its NoSQL features, and now offers a JSONB column type with which we can store a JSON blob. We’re going to add a column to our items table that uses this JSONB type and in it we’ll store allergy information about the menu items.

Start by creating the database migration:

 $ mix ecto.gen.migration add_allergy_info_to_menu_item

Add the column in the migration file:

 defmodule​ PlateSlate.Repo.Migrations.AddAllergyInfoToMenuItem ​do
 use​ Ecto.Migration
 
 def​ change ​do
  alter table(​:items​) ​do
  add ​:allergy_info​, ​:map
 end
 end
 end

Then expose it in Elixir by adding the field in the schema module:

 field ​:allergy_info​, {​:array​, ​:map​}

That’s it as far as the underlying data schema is concerned. Let’s run the database migration, and then add a menu item with allergy information:

 $ ​​mix​​ ​​ecto.migrate

In the book code for this chapter, we’ve added a new item, “Thai Salad,” that contains some allergy information. If you don’t want to reset your database, you can just copy and paste this into an iex -S mix session:

 alias PlateSlate.{Menu, Repo}
 category = Repo.get_by(Menu.Category, ​name:​ ​"​​Sides"​)
 %Menu.Item{
 name:​ ​"​​Thai Salad"​,
 price:​ 3.50,
 category:​ category,
 allergy_info:​ [
  %{​"​​allergen"​ => ​"​​Peanuts"​, ​"​​severity"​ => ​"​​Contains"​},
  %{​"​​allergen"​ => ​"​​Shell Fish"​, ​"​​severity"​ => ​"​​Shared Equipment"​},
  ]
 } |> Repo.insert!

Otherwise, a mix ecto.reset will clear everything out and re-run the seeds. Now that everything is here, let’s take a look at what the menu item actually looks like:

 iex> PlateSlate.Menu.Item |> PlateSlate.Repo.get_by(​name:​ ​"​​Thai Salad"​)
 %PlateSlate.Menu.Item{​__meta__:​ ​#Ecto.Schema.Metadata<:loaded, "items">,
 added_on:​ ​~​D[2017-08-27],
 allergy_info:​ [%{​"​​allergen"​ => ​"​​Peanuts"​, ​"​​severity"​ => ​"​​Contains"​},
  %{​"​​allergen"​ => ​"​​Shell Fish"​, ​"​​severity"​ => ​"​​Shared Equipment"​}],
 category:​ ​#Ecto.Association.NotLoaded<association :category is not loaded>,
 category_id:​ 2, ​description:​ nil, ​id:​ 9,
 inserted_at:​ ​~​N[2017-08-28 02​:39:37​.300521], ​name:​ ​"​​Thai Salad"​,
 price:​ ​#Decimal<3.5>,
 tags:​ ​#Ecto.Association.NotLoaded<association :tags is not loaded>,
 updated_at:​ ​~​N[2017-08-28 02​:39:37​.300525]}

As you can see, there is an allergy_info key in our Menu.Item struct now, and it contains a list of maps. Each map describes a particular allergen, giving its name and severity information. Some people only care if a particular allergen is an ingredient in the dish, whereas others must avoid anything that shares even preparation surfaces with that allergen.

Notice how the map on the allergy_info: key of the Thai salad is full of string keys and not atom keys—for example, "allergen" instead of :allergen. All we told Ecto is that the :allergy_info column is a :map, and since it can’t know anything about its internal structure, it just gives us plain, deserialized JSON. Let’s see how this messes up our default resolver, and what we can do to fix it.

Modeling this in our Absinthe schema starts off pretty simply. We need to add the allergy_info field to our menu_item object, and then we need to create a new object to model the information found there:

 object ​:menu_item​ ​do
 
  interfaces [​:search_result​]
 
  field ​:id​, ​:id
  field ​:name​, ​:string
  field ​:description​, ​:string
  field ​:price​, ​:decimal
  field ​:added_on​, ​:date
» field ​:allergy_info​, list_of(​:allergy_info​)
 end
 
»object ​:allergy_info​ ​do
» field ​:allergen​, ​:string
» field ​:severity​, ​:string
»end

We run into trouble, though, if we actually try to query this information in GraphiQL:

 mix phx.server
 {
  menuItems(filter: {name: ​"Thai Salad"​}) {
  allergyInfo { allergen severity }
  }
 }
images/chp.middleware/graphiql-1.png

All of the allergen and severity fields came back null!

What’s going on? We didn’t get any errors back from the server, and our code certainly looks valid. If we think back to how the default resolution behavior works, though, this makes sense. Our :allergen field, for example, is going to do a Map.get(parent, :allergen) call on the map inside the JSONB column, but of course there isn’t any such key there. :allergen is an atom, but all the keys in that map are strings. We can make this work by doing this:

 object ​:allergy_info​ ​do
  field ​:allergen​, ​:string​ ​do
  resolve ​fn​ parent, _, _ ->
  {​:ok​, Map.get(parent, ​"​​allergen"​)}
 end
 end
  field ​:severity​, ​:string​ ​do
  resolve ​fn​ parent, _, _ ->
  {​:ok​, Map.get(parent, ​"​​severity"​)}
 end
 end
 end

This is a bit tedious and verbose. Really what we want to do is change the default resolver for the fields defined on this object:

»def​ middleware(middleware, field, %{​identifier:​ ​:allergy_info​} = object) ​do
» new_middleware = {Absinthe.Middleware.MapGet, to_string(field.identifier)}
» middleware
» |> Absinthe.Schema.replace_default(new_middleware, field, object)
»end
 def​ middleware(middleware, _field, %{​identifier:​ ​:mutation​}) ​do
  middleware ++ [Middleware.ChangesetErrors]
 end
 def​ middleware(middleware, _field, _object) ​do
  middleware
 end

We’ll add an additional function head for middleware/3 that pattern matches for fields where the object definition’s identifier matches :allergy_info.

This new code sets up a new specification using the Absinthe.Middleware.MapGet middleware, and passes as its option a stringified version of our field identifier. The middleware will then use the string identifier (instead of an atom) to retrieve the correct value from the map. With this new middleware definition, we call the Absinthe.Schema.replace_default/4 function, which handles swapping it in for the existing default in the list.

We could just return [{Absinthe.Middleware.MapGet, to_string(field.identifier)}] from the function and be done with it, but the replace_default/4 function is more future-proof. This is both from the perspective of Absinthe itself, which may decide to change its default somewhere down the line, and also from the perspective of your own schema. In Chapter 9, Tuning Resolution, we’ll add some tracing middleware, and this function makes sure we don’t end up ignoring that.

Now if we try our query in GraphiQL again:

images/chp.middleware/graphiql-2.png

We get the expected result!

Middleware is an enormously important tool for keeping your resolvers focused, giving you an incredible amount of control over what happens when executing a document. It’s also a great feature for third-party packages that want to augment Absinthe’s resolution logic; the middleware/3 callback is a handy integration point.

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

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