Modeling Input Objects

Up to this point, we’ve been adding arguments directly onto our fields, but this can get messy. Imagine, for instance, if we wanted to add various filtering options to our :menu_items field. We could just add them à la carte:

 @desc ​"​​Matching a category name"
 arg ​:category​, ​:string
 @desc ​"​​Matching a tag"
 arg ​:tag​, ​:string
 
 @desc ​"​​Priced above a value"
 arg ​:priced_above​, ​:float
 
 @desc ​"​​Priced below a value"
 arg ​:priced_below​, ​:float

Mixed in with other arguments that we add to the field, this can quickly become a hodgepodge of various flags and options that would be better organized into related groupings. GraphQL gives us a tool to do this: input object types.

We can collect multiple arguments and model them as a special object type used just for argument values. Let’s take the :category, :tag, :priced_above, and :priced_below arguments and group them together into a new input object type, :menu_item_filter:

 @desc ​"​​Filtering options for the menu item list"
 input_object ​:menu_item_filter​ ​do
 
  @desc ​"​​Matching a name"
  field ​:name​, ​:string
 
  @desc ​"​​Matching a category name"
  field ​:category​, ​:string
 
  @desc ​"​​Matching a tag"
  field ​:tag​, ​:string
 
  @desc ​"​​Priced above a value"
  field ​:priced_above​, ​:float
 
  @desc ​"​​Priced below a value"
  field ​:priced_below​, ​:float
 
 end

We’ve taken the filters and placed them inside an input_object macro block that demarcates the limits of our new :menu_item_filter type. You’ll also notice that we’re not using arg anymore; just like normal object types, input objects model their members as fields, not arguments. Fields for input objects, however, don’t have any arguments (or a resolver) of their own; they’re merely there to model structure.

Let’s plug this new type in as an argument for the :menu_items field:

 field ​:menu_items​, list_of(​:menu_item​) ​do
» arg ​:filter​, ​:menu_item_filter
  arg ​:order​, ​type:​ ​:sort_order​, ​default_value:​ ​:asc
  resolve &Resolvers.Menu.menu_items/3
 end

To support the filter, we modify PlateSlate.Menu.list_items/1, reworking it to build a query using either or both of the :order and :filter arguments:

 def​ list_items(args) ​do
  args
  |> Enum.reduce(Item, ​fn
  {​:order​, order}, query ->
  query |> order_by({^order, ​:name​})
  {​:filter​, filter}, query ->
  query |> filter_with(filter)
 end​)
  |> Repo.all
 end
 
 defp​ filter_with(query, filter) ​do
  Enum.reduce(filter, query, ​fn
  {​:name​, name}, query ->
  from q ​in​ query, ​where:​ ilike(q.name, ^​"​​%​​#{​name​}​​%"​)
  {​:priced_above​, price}, query ->
  from q ​in​ query, ​where:​ q.price >= ^price
  {​:priced_below​, price}, query ->
  from q ​in​ query, ​where:​ q.price <= ^price
  {​:category​, category_name}, query ->
  from q ​in​ query,
 join:​ c ​in​ assoc(q, ​:category​),
 where:​ ilike(c.name, ^​"​​%​​#{​category_name​}​​%"​)
  {​:tag​, tag_name}, query ->
  from q ​in​ query,
 join:​ t ​in​ assoc(q, ​:tags​),
 where:​ ilike(t.name, ^​"​​%​​#{​tag_name​}​​%"​)
 end​)
 end

We use the order_by and where macros from Ecto.Query as we iterate over the key/value pairs of the filter and build up the query with Enum.reduce/3.[16] To understand how the filter can be sent to Absinthe, let’s build and run a couple of tests. In the first test, we’ll provide the filter as a literal:

 @query ​"""
 {
  menuItems(filter: {category: "Sandwiches", tag: "Vegetarian"}) {
  name
  }
 }
 """
 test ​"​​menuItems field returns menuItems, filtering with a literal"​ ​do
  response = get(build_conn(), ​"​​/api"​, ​query:​ @query)
  assert %{
 "​​data"​ => %{​"​​menuItems"​ => [%{​"​​name"​ => ​"​​Vada Pav"​}]}
  } == json_response(response, 200)
 end

Here we’re providing the filter argument value formatted just as you might expect from a JavaScript object, using curly braces and bare, unquoted identifiers for the field names. Once the filter argument passes schema checks, it is restructured to match the schema naming, handling camelCase to snake_case conversion, if appropriate. Not much needs to be done to the filter input object value; the arguments map passed to our menu items resolver looks exactly like the GraphQL document, using atom keys:

 %{​category:​ ​"​​Sandwiches"​, ​tag:​ ​"​​Vegetarian"​}

We’ve seen how input objects can be provided as literals, but usually complex arguments will be sent as variables, so let’s look at how this same request might look using a $filter variable:

 @query ​"""
 query ($filter: MenuItemFilter!) {
  menuItems(filter: $filter) {
  name
  }
 }
 """
 @variables %{​filter:​ %{​"​​tag"​ => ​"​​Vegetarian"​, ​"​​category"​ => ​"​​Sandwiches"​}}
 test ​"​​menuItems field returns menuItems, filtering with a variable"​ ​do
  response = get(build_conn(), ​"​​/api"​, ​query:​ @query, ​variables:​ @variables)
  assert %{
 "​​data"​ => %{​"​​menuItems"​ => [%{​"​​name"​ => ​"​​Vada Pav"​}]}
  } == json_response(response, 200)
 end

It looks very similar; as in previous examples, the variable value is sent alongside the query. Here, for the test case, the value of the filter variable is defined using an Elixir map type, which is how Absinthe will receive the value after the JSON object sent as part of the request is parsed by Plug.Parsers and the absinthe_plug package.

Having replaced the matching arg with the filter arg, you’ll also want to take a moment to go through the existing tests that are now failing and turn any matching: "..." inputs into filter: {name: "..."}. Pay attention to the error message you get back in the failing tests. Once again, we can see Absinthe at work validating input without any code necessary in our resolvers.

Rules for Input Objects

images/aside-icons/info.png

Here are some things to keep in mind when building input objects:

  • Input objects can be nested. You can define an input object field as having an input object type. This nesting can be arbitrarily deep.

  • Input object types, unlike normal object types, do not support circular references. You can’t have two input types that refer to each other, either directly or through an intermediary.

  • Input object type fields can be of any type that a field argument might use. It’s best to just think of them as structured arguments.

We’ve just scratched the surface of input objects. We’ll dig in deeper in Chapter 5, Making a Change with Mutations, where we’ll learn about how we can use input objects to model data for changesets.

In the meantime, let’s address non-null constraints, the mechanism that GraphQL schemas use to enforce that a given argument is provided in documents before they are executed.

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

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