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