Understanding Abstract Types

Up to this point, we’ve focused on building concrete types that closely model the underlying data, matching up with the Ecto schemas we’ve built for our PlateSlate application.

To support an easy-to-use, flexible API for your users, you’ll need to go beyond this type of modeling and learn how to use abstract types as well.

Let’s look at a quick example. A standard feature for a user interface (and the APIs that support them) is a search function. If we were going to implement a search function for our PlateSlate application, allowing users to retrieve both menu items and menu item categories (a grouping of menu items) that match a search term, how would we do it?

With only our concrete types in place, we’re stuck with having to build this feature as two separate fields. After all, a GraphQL field can only resolve to a single type. Here’s about the best that our users can hope for:

 query​ Search($term: String!) {
  searchCategories(matching: $term) {
 # Select fields from category results
  }
  searchMenuItems(matching: $term) {
 # Select fields from menu item results
  }
 }

We’ll have to define a distinct search field for every type we want to be searchable, looking something like this in our schema:

 field ​:search_categories​, list_of(​:category​) ​do
  arg ​:matching​, non_null(​:string​)
  resolve ​fn​ _, %{​matching:​ term}, _ ->
 # Search logic
  {​:ok​, results}
 end
 end
 
 field ​:search_menu_items​, list_of(​:menu_item​) ​do
  arg ​:matching​, non_null(​:string​)
  resolve ​fn​ _, %{​matching:​ term}, _ ->
 # Similar search logic for a similar field
  {​:ok​, results}
 end
 end

This is going to get tedious. Imagine what life will be like in six months after we’ve fully built out the domain to include allergen information, listings for our various restaurant locations, marketing content we’d like searchable, and so on. A brittle, complicated mess.

What if, instead, we could model all these search results as…search results? It would sure look better if our schema code could read like this:

 field ​:search​, list_of(​:search_result​) ​do
  arg ​:matching​, non_null(​:string​)
  resolve ​fn​ _, %{​matching:​ term}, _ ->
 # Combined search logic, returning heterogenous data
  {​:ok​, results}
 end
 end

This would make our search feature more adaptable as features are added later. In any case, the code is certainly easier to read, and it would allow user queries to look more like this:

 query​ Search($term: String!) {
  search(matching: $term) {
 # Select fields from a mix of search results
  }
 }

Look, no type-specific fields! Just a single field that users can leverage anytime they want to retrieve records by a search term.

Now, to do this, we need to let Absinthe know what a :search_result is and how it relates to the concrete types that we want to be searchable.

We’re going to cover a couple of different options that the GraphQL specification gives us: unions and interfaces. After we’re done, you should have a solid grounding in both abstract type mechanisms and feel confident about when to use each when modeling your domain model in future applications.

Using Unions

A GraphQL union type is an abstract type that represents a set of specific concrete types. For instance, in our PlateSlate search example, a :search_result could be a union type for both :menu_item and :category.

Let’s define that in our schema…but first, we need to add the :category type. It’s a straightforward grouping of :menu_item records with a name and description:

 alias PlateSlateWeb.Resolvers
 
 object ​:category​ ​do
  field ​:name​, ​:string
  field ​:description​, ​:string
  field ​:items​, list_of(​:menu_item​) ​do
  resolve &Resolvers.Menu.items_for_category/3
 end
 end

We’ve included a resolver to load the menu items for a category when the :items field is selected. At the moment, the implementation is a bit naive:

 def​ items_for_category(category, _, _) ​do
  query = Ecto.assoc(category, ​:items​)
  {​:ok​, PlateSlate.Repo.all(query)}
 end

If the menu items for a list of categories were requested, for example, this would execute a database query per category (an example of the infamous “N+1” problem). This isn’t something we’d want in production, and you’ll learn how to combat it later in Chapter 9, Tuning Resolution.

Notably, though, this is the first resolver we’ve written where we’re using the first argument, which receives the parent value. In our case, this resolver is on the :items field of the :category object, so its parent value is a category. We can then use that category to do a database query for its items.

Now that the object type is out of the way, let’s move on and define the :search_result union type:

 union ​:search_result​ ​do
  types [​:menu_item​, ​:category​]
 # Almost done...
 end

This uses a couple of new macros from Absinthe. The union macro is used to create our type, and it works a lot like object. The types macro, used inside the union scope, sets its types.

We need to add one more thing to our type definition. Abstract types like unions (and, as you’ll learn about later, interfaces) need a way to determine the concrete type for a value. For our :search_result union type, supporting both :menu_item and :category concrete types, we’ll write it like this:

 union ​:search_result​ ​do
  types [​:menu_item​, ​:category​]
  resolve_type ​fn
  %PlateSlate.Menu.Item{}, _ ->
 :menu_item
  %PlateSlate.Menu.Category{}, _ ->
 :category
  _, _ ->
  nil
 end
 end

The resolve_type macro takes a 2-arity function. The first parameter of the function will receive the value that we’re checking, and the second parameter will receive the resolution information (which we’ll just ignore in this case). Recall that a union type means “one of these types.” When Absinthe is actually running a document and getting Elixir values, it needs a way to figure out which Elixir value maps to which of the types in the union, and that’s what this resolve_type function does for us.

Since the resolved value for the :search_result type will be Ecto schema structs for PlateSlate.Menu.Item or PlateSlate.Menu.Category, determining the associated Absinthe type is straightforward. For completeness, we provide a fall-through match. It returns nil, which denotes that the value doesn’t belong to any member type of the union.

Now that we’ve completed the modeling for the :search_result type, let’s build that search field we’ve been thinking about. We’ll add it to our query block in the schema file:

 query ​do
 # Other query fields
 
  field ​:search​, list_of(​:search_result​) ​do
  arg ​:matching​, non_null(​:string​)
  resolve &Resolvers.Menu.search/3
 end
 
 end

To resolve the field, we’ll use a search resolver function.

 def​ search(_, %{​matching:​ term}, _) ​do
  {​:ok​, Menu.search(term)}
 end

It just hands off the work to a context function, which runs a search pattern against names and descriptions for each table, and returns the combined results.

 @search [Item, Category]
 def​ search(term) ​do
  pattern = ​"​​%​​#{​term​}​​%"
  Enum.flat_map(@search, &search_ecto(&1, pattern))
 end
 
 defp​ search_ecto(ecto_schema, pattern) ​do
  Repo.all from q ​in​ ecto_schema,
 where:​ ilike(q.name, ^pattern) ​or​ ilike(q.description, ^pattern)
 end

While each value returned from this function is a :menu_item or :category, it’s also a valid :search_result, owing to the union type definition we added to the schema.

There’s one concern that this neat new ability to generalize a field result brings up: if a field can return different results with different shapes, how can users effectively select the data they want in the query?

Let’s look at that search query again:

 query​ Search($term: String!) {
  search(matching: $term) {
 # How do we differentiate category and menu item fields?
  }
 }

To do this, we make use of an important GraphQL feature, fragments. Fragments are a way to write chunks of GraphQL that can target a specific type.

Here’s a query that pulls out some information specific to PlateSlate menu items and categories:

1: query​ Search($term: String!) {
search(matching: $term) {
... ​on​ MenuItem {
name
5:  }
... ​on​ Category {
name
items {
name
10:  }
}
}
}

You can see where we’re defining and inserting fragments on lines 3 and 6. The ... is referred to as a “fragment spread,” and it inserts the inline fragment that follows. This is nomenclature you’ll also find in ECMAScript 6 objects,[17] which isn’t surprising considering the number of JavaScript developers involved with the creation and maintenance of GraphQL.

The inline fragment targets a type (introduced with on) and defines the set of fields, within the curly braces, that apply for any item that matches the type. You’ll learn about named fragments later in this chapter.

Let’s explore this query a bit with the GraphiQL user interface to see what the results look like.

We’ve made the search function ridiculously permissive, allowing even single character searches, so let’s make use of that. By searching for anything that matches "e", we should be able to get a large volume of results.

First, we start the server:

 $ ​​mix​​ ​​phx.server
 [info] Running PlateSlateWeb.Endpoint with Cowboy using http://0.0.0.0:4000

Heading over to http://localhost:4000/graphiql in our browser, we enter the query in the left side panel, define our term variable, and press the “play” button:

images/chp.flexibility/graphiql-1.png

In the bottom-right pane, you can see those results: a mix of menu items and categories.

We can make the difference between the results more obvious by using a handy GraphQL tool: introspection.

Introspecting Value Types

The GraphiQL interface uses GraphQL’s introspection[18] capabilities extensively to provide nice features like autocompletion and documentation. We can use introspection ourselves, too. Let’s decorate our search query with a little introspection:

 query​ Search($term: String!) {
  search(matching: $term) {
  ... ​on​ MenuItem {
  name
  }
  ... ​on​ Category {
  name
  items {
  name
  }
  }
» __typename
  }
 }

Fields that begin with __ are reserved by GraphQL to support features like introspection. The __typename introspection field that we’re using here always returns the concrete GraphQL type name that’s in the associated scope.

If we plug it back into our GraphiQL example, we can see it in action:

images/chp.flexibility/graphiql-2.png

Now we can see our results handily annotated with the GraphQL types in the result pane.

Use __typename to See GraphQL Types

images/aside-icons/tip.png

If you’re ever curious about the GraphQL type that’s being returned, use the built-in __typename introspection field. It will always return the concrete GraphQL type for the surrounding scope, which can be handy for debugging an API or updating client-side caches.

At this point, we have a fully functional search (although one we’ll want to tune before we release it to production), and we’ve played with it in GraphiQL. Let’s build a test for it, too. We’ll shorten up our query a bit and just make sure that we get a mix of menu items and categories returned:

 @query ​"""
 query Search($term: String!) {
  search(matching: $term) {
  ... on MenuItem { name }
  ... on Category { name }
  __typename
  }
 }
 """
 @variables %{​term:​ ​"​​e"​}
 test ​"​​search returns a list of menu items and categories"​ ​do
  response = get(build_conn(), ​"​​/api"​, ​query:​ @query, ​variables:​ @variables)
  assert %{​"​​data"​ => %{​"​​search"​ => results}} = json_response(response, 200)
  assert length(results) > 0
  assert Enum.find(results, &(&1[​"​​__typename"​] == ​"​​Category"​))
  assert Enum.find(results, &(&1[​"​​__typename"​] == ​"​​MenuItem"​))
 end

We’ll run mix test to execute our test:

 $ ​​mix​​ ​​test​​ ​​test/plate_slate_web/schema/query/search_test.exs
 .
 
 Finished in 0.2 seconds
 1 test, 0 failures

It passes; great!

Now let’s think a little about how we could make this even better. The simplified search that we’ve done for our test seems to hint at a sore point: there sure is a lot of duplication involved to get the same field from two different concrete types.

Because unions are about combinations of disparate types that might not have any fields in common, retrieving data from them requires us to use fragments (that target types) to get the data we want. There’s another option: interfaces. Let’s see if modeling our search results as an interface might make things a bit simpler.

Using Interfaces

GraphQL interfaces are similar to unions, with one key difference: they add a requirement that any member types must define a set of included fields. This might remind you of interfaces in other languages (or perhaps behaviours in Elixir/Erlang).

For search results, we know that we want to easily access the name field. It’s fair to say that a search result should always include a name; it’s a simple constraint, and one that doesn’t require us to add any fields to our :menu_item and :category types. We just need to convert our :search_result to an interface and indicate that our types belong to it.

Let’s open up the file with our Absinthe type definitions and make the modifications. First, we’ll convert our :search_result type to an interface:

 interface ​:search_result​ ​do
  field ​:name​, ​:string
  resolve_type ​fn
  %PlateSlate.Menu.Item{}, _ ->
 :menu_item
  %PlateSlate.Menu.Category{}, _ ->
 :category
  _, _ ->
  nil
 end
 end

We use the interface macro instead of the union macro, and because interfaces need to be able to resolve the concrete types of their values just like unions, we get to keep the resolve_type function that we already defined.

We removed the types macro usage, which our union type used to declare which types it included; the object types that implement our new interface declare that themselves, as we’ll see in a moment.

The only addition we’ve made involves the use of the field macro. Here we’ve indicated any implementing object types must define a :name field that returns a :string value. Easily done, as both :menu_item and :category already do that. All we need to do with them is declare they implement our new interface.

We’ll make the addition to :menu_item:

 object ​:menu_item​ ​do
» interfaces [​:search_result​]
  field ​:id​, ​:id
  field ​:name​, ​:string
  field ​:description​, ​:string
  field ​:added_on​, ​:date
 end

And to :category:

 alias PlateSlateWeb.Resolvers
 
 object ​:category​ ​do
» interfaces [​:search_result​]
  field ​:name​, ​:string
  field ​:description​, ​:string
  field ​:items​, list_of(​:menu_item​) ​do
  resolve &Resolvers.Menu.items_for_category/3
 end
 end

As you can probably guess from the list value we’re passing to interfaces, an object type can implement as many interfaces as you’d like.

Now that we have these changes in place, let’s see what difference it can make to our query documents. We’ll update the test we just added, but first it’s worth pointing out that it should still pass with flying colors:

 $ ​​mix​​ ​​test​​ ​​test/plate_slate_web/schema/query/search_test.ex
 .
 
 Finished in 0.2 seconds
 1 test, 0 failures

We’ll make our change to the search query, removing the now-excessive fragment usage:

 @query ​"""
 query Search($term: String!) {
  search(matching: $term) {
  name
  __typename
  }
 }
 """
 @variables %{​term:​ ​"​​e"​}
 test ​"​​search returns a list of menu items and categories"​ ​do
  response = get(build_conn(), ​"​​/api"​, ​query:​ @query, ​variables:​ @variables)
  assert %{​"​​data"​ => %{​"​​search"​ => results}} = json_response(response, 200)
  assert length(results) > 0
  assert Enum.find(results, &(&1[​"​​__typename"​] == ​"​​Category"​))
  assert Enum.find(results, &(&1[​"​​__typename"​] == ​"​​MenuItem"​))
  assert Enum.all?(results, &(&1[​"​​name"​]))
 end

Then, run the tests again:

 $ ​​mix​​ ​​test​​ ​​test/plate_slate_web/schema/query/search_test.exs
 .
 
 Finished in 0.2 seconds
 1 test, 0 failures

See how the name field is bare, without a wrapping ... on Type { } inline fragment? This works because selecting fields that have been declared on the interface aren’t subject to the same type of restrictions as selecting fields on unions (which have no such mechanism).

This doesn’t mean, of course, that you can just select any field on an interface and get away with it. If we wanted to retrieve information about menu items that belonged to any categories that were returned from a search, we’d still need to have a wrapping fragment type, like this:

 query​ Search($term: String!) {
  search(matching: $term) {
  name
  ... ​on​ Category {
  name
  items {
  name
  }
  }
  }
 }

This is because :items isn’t declared on the interface. It’s not a field that’s shared with other object types (that is, :menu_item) that implement :search_result.

Interfaces are a handy tool, and they’re often the right choice when you need an abstract class precisely for the reason that we’ve illustrated here. If there are fields in common, interfaces allow users to write more simple, readable GraphQL.

Let’s talk about another tool along those lines: named fragments. Wouldn’t it be nice if we could reuse chunks of GraphQL rather than have the same thing over and over?

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

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