Defining Field Arguments

GraphQL documents are made up of fields. The user lists the fields they would like, and the schema uses its definition of those fields to resolve the pieces of data that match. The system would be pretty inflexible if it did not also allow users to provide additional parameters that would clarify exactly what information each field needed to find. A user requesting information about menuItems, for instance, may want to see certain menu items or a certain number of them.

It’s for this reason that GraphQL has the concept of field arguments: a way for users to provide input to fields that can be used to parameterize their queries. Let’s take a look at our example application and see how we can extend our Absinthe schema by defining the arguments that our API will accept for a field, and then see how we can use those arguments to tailor the result for users.

We’ve already built a field in our API that we could make more flexible by accepting user input: the list of menu items. Our schema’s menuItems field, if you remember, looks something like this:

1: alias PlateSlate.{Menu, Repo}
query ​do
5:  field ​:menu_items​, list_of(​:menu_item​) ​do
resolve ​fn​ _, _, _ ->
{​:ok​, Repo.all(Menu.Item)}
end
end
10: 
end

On line 7, the field’s resolver just returns all the menu items, without any support for filtering, ordering, or other modifications to the scope or layout of the result. The field isn’t declaring any arguments, so the resolver doesn’t receive anything with which we could modify the list of menu items retrieved.

Let’s add an argument to our schema to support filtering menu items by name. We’ll call it matching, then configure our field resolver to use it when provided:

1: alias PlateSlate.{Menu, Repo}
import​ Ecto.Query
query ​do
5: 
field ​:menu_items​, list_of(​:menu_item​) ​do
arg ​:matching​, ​:string
resolve ​fn
_, %{​matching:​ name}, _ ​when​ is_binary(name) ->
10:  query = from t ​in​ Menu.Item, ​where:​ ilike(t.name, ^​"​​%​​#{​name​}​​%"​)
{​:ok​, Repo.all(query)}
_, _, _ ->
{​:ok​, Repo.all(Menu.Item)}
end
15: end
end

On line 7, we defined matching as a :string type. If you remember from the previous chapter, :string is a built-in type. We can use it as an input type, too.

We’re not making the matching argument mandatory here, so we need to support resolving our menuItems field in the event it’s provided, and in the event it isn’t. You can see Elixir’s pattern matching capability used in the two separate function heads of our resolver to handle those two cases.

The second function head, on line 12, serves as the fall-through match and is identical to our original resolver.

It’s the first function head, on line 9, that adds our new behavior. On line 10, we make use of the matched argument as name (in the from macro that Ecto.Query[14] provides) to build our Ecto query. We pulled the Ecto.Query macros in on line 2. By declaring our inputs up front, Absinthe has a bounded set of inputs to work with and can thus give us an atom-keyed map to work with as arguments, unlike Phoenix controller action params.

Resolvers and Field Arguments

images/aside-icons/tip.png

Absinthe only passes arguments to resolvers if they have been provided by the user. Making a map key match of the arguments resolver function parameter is a handy way to check for a field argument that’s been specified in the request.

Writing complicated resolvers as anonymous functions can have a negative side effect on a schema’s readability, so to keep the declarative look and feel of the schema alive and well, let’s do a little refactoring and extract the resolver into a new module. Because filtering menu items is an important feature of our application—and could be used generally, not just from the GraphQL API—we’ll also pull the core filtering logic into the PlateSlate.Menu module, which is where our business logic relating to the menu belongs.

Here’s our new resolver module:

 defmodule​ PlateSlateWeb.Resolvers.Menu ​do
  alias PlateSlate.Menu
 
 def​ menu_items(_, args, _) ​do
  {​:ok​, Menu.list_items(args)}
 end
 end

You can see that the resolver is calling PlateSlate.Menu.list_items/1, passing the arguments. The logic inside PlateSlate.Menu looks like this:

 def​ list_items(%{​matching:​ name}) ​when​ is_binary(name) ​do
  Item
  |> where([m], ilike(m.name, ^​"​​%​​#{​name​}​​%"​))
  |> Repo.all
 end
 def​ list_items(_) ​do
  Repo.all(Item)
 end

This code should look pretty familiar; it’s been extracted out of our anonymous resolver function and restructured into a named function. Doing this makes both the resolver and the overall schema more readable.

Now let’s wire our resolver back into our :menu_items field in the schema:

 alias PlateSlateWeb.Resolvers
 
 query ​do
 
  field ​:menu_items​, list_of(​:menu_item​) ​do
  arg ​:matching​, ​:string
  resolve &Resolvers.Menu.menu_items/3
 end
 
 end

Using Elixir’s & function capture special form[15] here lets us tie in the function from our new module as the resolver for the field and keeps the schema declaration tight and focused.

Let’s explore using this new field argument that we’ve defined with some GraphQL queries that cover a range of scenarios.

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

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