Absinthe Tips and Tricks

Absinthe is a great package for building GraphQL api’s in Elixir. It is fast, good DSL and overall great implementation of the GraphQL spec for servers.

Working with Absinthe I’ve come across some patterns that have helped clean up code. I’d like to share some of them here.

The Self function

Imagine you have an image object you want to expose, and for each image you want a separate object with just the dimensions of the image. In the GraphQL SDL it would look like

type Image {
   dimensions: Dimensions!
   name: String!
   size: Int!
}

type Dimensions{
   width: Int!
   height: Int!
}

However our the struct that feeds the image resolver looks like

%{
   name: "image.jpeg",
   width: 100,
   height: 150,
   size: 2000
}

In Absinthe we could model it as follows

object :image do
  field(:name, non_null(:string))
  field(:size, non_null(:integer))

  field :dimensions, non_null(:dimensions) do
    resolve(fn image, _, _ ->
      {:ok,
       %{
         width: image.width,
         height: image.height
       }}
    end)
  end
end

object :dimensions do
  field(:width, non_null(:integer))
  field(:height, non_null(:integer))
end

The pattern of having a struct represent a graphql object and its child object is pretty common. So we can also make this more generic like so:

  object :image do
     field(:name, non_null(:string))
     field(:size, non_null(:integer))
     field(:dimensions, non_null(:dimensions), resolve: self())
  end
  ...
  defp self() do
  	fn parent, _, _ -> {:ok, parent} end
  end

We call the self() function and return a resolver with an arity of 3. The only thing it does is take its first argument and return it with an ok-tuple. Having a function return a resolver is a common pattern to make your schemas and resolvers more succinct.

The viewerCan… functions

It is common for web applications that the logged in user has permissions on certain objects that he doesn’t have on others. E.g. the user can edit his/her own comments but not those of others. In GraphQL we may want to expose that information on a comment.

For example, we have this schema

type Comment {
   content: String!
   author: User!
   viewerCanEdit: Boolean!
}
type User {
   fullName: String
}

In the above schema, each comment has an author. When we query this we want to know whether the logged in user has permission to edit this particular comment. The assumption is made that the current user is set on the Absinthe context map.

  object :comment do
     field(:name, non_null(:string))
     field(:author, non_null(:user))
     field :viewerCanEdit, non_null(:boolean) do
     	resolve(fn comment, _, %{context: %{current_user: %{id: id}}} ->
        	{:ok, id == comment.author_id}
        end
     end
  end
  
  object :user do
     field(:full_name, non_null(:string))
  end

So we check whether the author_id of the comment is the same as the id of the current user.

Again, we can make this more generic by using a function returning a resolver.

  object :comment do
     field(:name, non_null(:string))
     field(:author, non_null(:user))
     field(:viewerCanEdit, non_null(:boolean), resolve: is_viewer(:author_id))
  end
  ...
  
  defp is_viewer(field_name) do
    fn
      parent, _, %{context: %{current_user: %{id: id}}} ->
        {:ok, id == Map.get(parent, field_name)}

      _, _, _ ->
        {:ok, false}
    end
  end

We pass in the field we want to compare to the is_viewer/1 function and fetch this field on the parent (in this case the comment) and compare it to the currently logged in user.

Renaming a field

Sometimes the (database) struct we supply to the graphql object has a different name for some field than the one we want to expose in our api. Say we have a name field in the database that we want to expose as fullName. So, the data we supply is as follows (e.g. coming from the database):

%User{
   name: "John Doe"
 }

And this is the schema

type User {
   fullName: String
}

There are two ways to go about this. The first is simply renaming it in the Absinthe DSL using name

  object :user do
     field(:name, non_null(:string), name: "full_name")
  end

This instructs Absinthe to fetch the name field from the struct but expose it as fullName.

The second method does it the other way around

  object :user do
     field(:full_name, non_null(:string), resolve: alias(:name))
  end
  
  def alias(field) do
    fn root, _, _ -> {:ok, Map.get(root, field)} end
  end

It uses a simple resolver function to fetch the name field from the struct instead of the fullName property of the user object.

I think these examples show how you can use simple functions to make your Absinthe graphql types and schemas a lot simpler and more declarative. I’m sure you can think of other clever ways of building function that return resolvers whenever you come across a common pattern.

Photo by Samuel Zeller on Unsplash