Using Enumeration Types

A GraphQL enumeration (or enum, as it’s generally called) is a special type of scalar that has a defined, finite set of possible values. Here are some examples of values that are well represented by enums:

  • Available shirt sizes: S, M, L, and XL
  • Color components: RED, GREEN, and BLUE
  • Ordering: ASC and DESC

Enums are a good choice if the possible values are well defined and unlikely to change, if those values are short (one or maybe two words long), and if they’re not a pair of values that are more clearly represented by a boolean flag.

Let’s use the ASC and DESC ordering example for a list of menu items that we’ll allow users to retrieve in ascending or descending order.

We’ll start by adding our enum type, :sort_order, using the enum and value macros. The enum macro works just like object, but it defines an enumeration instead of an object. The value macro defines a possible value of the enum. For our use case, :asc and :desc will do:

 enum ​:sort_order​ ​do
  value ​:asc
  value ​:desc
 end

To allow users to dictate a :sort_order for our :menu_items field, we need to declare a new argument. We’ll call it :order, too:

 field ​:menu_items​, list_of(​:menu_item​) ​do
  arg ​:matching​, ​:string
» arg ​:order​, ​:sort_order
  resolve &Resolvers.Menu.menu_items/3
 end

There are a few things to notice about the argument declaration. First, as with the :matching argument for the :menu_item type, the standard form for an argument declaration is arg NAME, TYPE. If this seems a bit confusing, we can make the type more explicit by providing it as a :type option. Let’s do that, and provide the argument a default value for good measure:

 arg ​:order​, ​type:​ ​:sort_order​, ​default_value:​ ​:asc

The second argument to the arg macro can be a keyword list to support additional options. Here we dictate that the :order argument should have a default value of :asc if one is not provided; that way, our users don’t have to declare an order if they don’t care about it, but our resolver can always expect to find a value for it. Let’s modify the PlateSlate.Menu.list_items/1 to handle those two cases:

  • When the order is given (or it defaults) as :asc
  • When the order is given as :desc
 def​ list_items(filters) ​do
  filters
  |> Enum.reduce(Item, ​fn
» {_, nil}, query ->
» query
» {​:order​, order}, query ->
» from q ​in​ query, ​order_by:​ {^order, ​:name​}
  {​:matching​, name}, query ->
  from q ​in​ query, ​where:​ ilike(q.name, ^​"​​%​​#{​name​}​​%"​)
 end​)
  |> Repo.all
 end

Here we use Ecto’s order_by to take the value of our :order argument directly—since it just happens to use :desc and :asc for ordering. We execute the query to retrieve the menu items, which are then returned. If we run the tests at this point, you’ll notice our default order is causing a failure:

 $ ​​mix​​ ​​test​​ ​​test/plate_slate_web/schema/query/menu_items_test.exs
 .
 1) test menuItems field returns menu items
  (PlateSlateWeb.Schema.Query.MenuItemsTest)
  test/plate_slate_web/schema/query/menu_items_test.exs:16
  Assertion with == failed
  Lots of details
 ...
 
 Finished in 0.3 seconds
 4 tests, 1 failure

This happens because the test doesn’t provide a desired order, so the order falls back to the default value. This causes the menu items to be returned in an order the test doesn’t expect. If we change the order of the menu items in the test to match the default value, the test will pass:

 test ​"​​menuItems field returns menu items"​ ​do
  conn = build_conn()
  conn = get conn, ​"​​/api"​, ​query:​ @query
  assert json_response(conn, 200) == %{​"​​data"​ => %{​"​​menuItems"​ => [
  %{​"​​name"​ => ​"​​Bánh mì"​},
  %{​"​​name"​ => ​"​​Chocolate Milkshake"​},
  %{​"​​name"​ => ​"​​Croque Monsieur"​},
  %{​"​​name"​ => ​"​​French Fries"​},
  %{​"​​name"​ => ​"​​Lemonade"​},
  %{​"​​name"​ => ​"​​Masala Chai"​},
  %{​"​​name"​ => ​"​​Muffuletta"​},
  %{​"​​name"​ => ​"​​Papadum"​},
  %{​"​​name"​ => ​"​​Pasta Salad"​},
  %{​"​​name"​ => ​"​​Reuben"​},
  %{​"​​name"​ => ​"​​Soft Drink"​},
  %{​"​​name"​ => ​"​​Vada Pav"​},
  %{​"​​name"​ => ​"​​Vanilla Milkshake"​},
  %{​"​​name"​ => ​"​​Water"​}
  ]}}
 end

We also want to make sure specifying a sort order works. Let’s write another test that will attempt to get the list of menu items, ordered descending, and check the name of the first menu item returned:

 @query ​"""
 {
  menuItems(order: DESC) {
  name
  }
 }
 """
 test ​"​​menuItems field returns items descending using literals"​ ​do
  response = get(build_conn(), ​"​​/api"​, ​query:​ @query)
  assert %{
 "​​data"​ => %{​"​​menuItems"​ => [%{​"​​name"​ => ​"​​Water"​} | _]}
  } = json_response(response, 200)
 end

We’re providing the order as DESC, and without quotes. By convention, enum values are passed in all uppercase letters; the value macro that we used to declare the enum values sets up a mapping for us, accepting enum values as literals and variables in all uppercase and converting them to atoms automatically.

Unconventional Enum Values

images/aside-icons/warning.png

While the value macro does support customizing the external representation used for enum values, the GraphQL specification explicitly recommends the uppercase convention that Absinthe sets up for you automatically.

Unless you have a very good reason, you should stick with the recommendation given by the specification so that your API provides as comfortable an experience as possible to users that are already familiar with GraphQL.

You provide variable values for enum types just as you do for String. Let’s revisit the test we just ran, but using variables to insert the argument value:

 @query ​"""
 query ($order: SortOrder!) {
  menuItems(order: $order) {
  name
  }
 }
 """
 @variables %{​"​​order"​ => ​"​​DESC"​}
 test ​"​​menuItems field returns items descending using variables"​ ​do
  response = get(build_conn(), ​"​​/api"​, ​query:​ @query, ​variables:​ @variables)
  assert %{
 "​​data"​ => %{​"​​menuItems"​ => [%{​"​​name"​ => ​"​​Water"​} | _]}
  } = json_response(response, 200)
 end

Because the "Water" menu item is the first item in descending order, we’re checking that it’s the first item returned.

Notice the type name that we’re using for our :sort_order enum variable, $order. It starts with SortOrder, as you’d expect given the way Absinthe type identifiers are automatically converted to title case for their canonical GraphQL names—but it ends in an exclamation mark (!). This denotes that, as the person writing the GraphQL query document, you’re making the variable mandatory. This is a handy tool on the client side, giving front-end developers the ability to enforce additional input constraints. A document that doesn’t meet its variable requirements won’t be executed if it’s received by Absinthe, and some client-side frameworks even enforce variable checks to prevent inadequately filled GraphQL documents from being sent at all.

We can make arguments mandatory at the schema level as well, adding non-null constraints to our argument types. We’ll cover that shortly, but first let’s take a look at how we can organize field arguments into groups using a mechanism that GraphQL calls input object types.

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

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