Organizing a Schema

Absinthe schemas are compiled, meaning that their types are collected, references are resolved, and the overall structure of the schema is verified against a set of rules during Elixir’s module compilation process. Absinthe does this to ensure that GraphQL documents can be processed at runtime using a schema module that’s already been checked for common errors and has been optimized for better performance.

That doesn’t mean your Absinthe schema needs to be written in a single module. In fact, when a schema grows beyond being a limited sketch of our domain model into something more comprehensive—when it becomes something that we need to maintain—it’s a good idea to organize it across multiple modules.

To do this, we need something to wire them all together so that Absinthe can find the portions of the schema we’ve extracted and organized elsewhere.

Thankfully, Absinthe provides two simple tools to help us: import_types and import_fields. Let’s look at how we can use these two handy macros in our schema definitions, using the PlateSlate example application that we’ve been working on.

Importing Types

During the module compilation process, all the types referenced in an Absinthe schema are bundled together and built into the compiled module. Right now, in our PlateSlate application, all the types are located inside a single module, PlateSlateWeb.Schema, and it just works. Unfortunately, it’s getting a bit long and unwieldy.

Let’s see about splitting the custom types from the root type definitions (like query). Since all the types we’ve built so far are menu-related, we’ll create a new module, PlateSlateWeb.Schema.MenuTypes, to hold them. Here’s what that looks like:

 defmodule​ PlateSlateWeb.Schema.MenuTypes ​do
 use​ Absinthe.Schema.Notation
 
  @desc ​"​​Filtering options for the menu item list"
  input_object ​:menu_item_filter​ ​do
 # menu item filter fields
 end
 
  object ​:menu_item​ ​do
 # menu fields
 end
 
 end

Absinthe refers to modules like this one as type modules, because their purpose is to contain a set of types for inclusion in a schema. Type modules can be kept for use in your own schema or packaged and published for reuse by others.

It’s important to note that unlike a schema module, which makes use of Absinthe.Schema, type modules use Absinthe.Schema.Notation instead. This gives them access to the general type definition macros (like object), without the top-level compilation and verification mechanism that only schemas need.

Use Absinthe.Schema.Notation in Type Modules

images/aside-icons/tip.png

Use Absinthe.Schema.Notation in your type modules to import Absinthe’s type definition macros. Don’t use Absinthe.Schema; it’s reserved for schema modules themselves.

Inside of our schema we use the import_types/1 macro and point it at our new module so that the newly extracted types are still usable from within our schema:

 defmodule​ PlateSlateWeb.Schema ​do
 use​ Absinthe.Schema
 
  alias PlateSlateWeb.Resolvers
 
» import_types __MODULE__.MenuTypes
 
  query ​do
 
  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
 
 end
 
 # Common types; :date, :sort_order, etc
 end

During compilation, Absinthe will pull in the type definitions from PlateSlate- Web.Schema.MenuTypes, wiring them into our schema module so they work just like they did when they were defined in place.

Notice that we’ve kept the root query object type around. The query macro is defined in Absinthe.Schema, and can only be used in our schema module. This is to ensure that we don’t end up with multiple root query object types when importing different type modules. The same restriction will apply to the other root types that we’ll define later: mutation and subscription.

Only Use import_types at the Schema Level

images/aside-icons/warning.png

Absinthe’s import_types macro should only be used from your schema module. Think of your schema module like a manifest, defining the complete list of type modules needed to resolve type references.

This has really helped clean up our schema, and we can take it even further if we need to later on. Let’s talk a bit about Absinthe’s other schema structural macro, import_fields.

Importing Fields

Imagine what it might be like down the road for our PlateSlate application when the surface area of the API has expanded to support a wide range of applications. Our user interface will only be the beginning; once we’ve opened up the API to third-party developers and support integrations with other services, the entry points into our API—the catalog of fields present inside our root query object type—might grow to the point that our schema once again becomes unwieldy, despite our best efforts refactoring other types into type modules.

To support breaking up a large object, Absinthe provides another macro, import_fields, that we can use.

In our hypothetical, successful future for PlateSlate, let’s say our root query object type has fields that provide the following:

  • Information about menu items
  • Specialized search functions for allergens
  • Customer and order history queries
  • Staff schedule details
  • Restaurant location address information for mapping

Instead of having an exhaustive query object in our schema.ex spanning dozens or hundreds of lines, what if it could look like this:

 query ​do
  import_fields ​:menu_queries
  import_fields ​:allergen_queries
  import_fields ​:customer_queries
  import_fields ​:staff_queries
  import_fields ​:location_queries
 # Other fields
 end

Instead of defining the fields directly in the root query object type, we can pull them out and put them into separate types (which we can place in other type modules). Here’s how our :menu_queries type might look, defining the same :menu_items field we’ve gotten used to seeing in our schema:

 object ​:menu_queries​ ​do
 
  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
 
 # Other menu-related fields
 
 end

It’s just an object type definition—nothing special—and we’d locate it alongside other menu-related objects in our menu_types.ex file. Instead of being used as a type for a field’s resolution, however, the :menu_queries object type just serves as a convenient named container to hold the fields we’d like to pull into our root query object type.

Deciding on Structure

The tools that we’ve covered here—import_types and import_fields—don’t establish any structural constraints for the way that you arrange your Absinthe-related modules. It’s completely up to you.

We opted for extracting the types along the same lines as our Ecto schemas are organized under the PlateSlate module; the types present in PlateSlate.Menu are represented by GraphQL types located in PlateSlateWeb.Schema.MenuTypes.

Now that you have some ideas about the tools and techniques you can use to arrange your Absinthe schema, let’s dig into another important real-world feature set: how to use abstract types to make your schema more flexible and reusable.

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

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