Elixir - Ecto / Absinthe Enums Recipe

 

Jon Ryser

Jon is an experienced, results-driven engineer who writes great code. He is confident and experienced at managing the unexpected.

Updated Mar 18, 2020

The need

I wanted to create some enums that could be built into a Postgres DB and also referenced through Ecto. These same enums needed to be referenced in Absinthe when creating GraphQL responses for a frontend. This would allow the frontend to query GraphQL directly for types and derivation of enums, ultimately allowing me to define the enums in one place only and eliminate the need to maintain them in multiple locations.

Now, it should be noted that I am new to Elixir. I really am loving it! I came up with a solution to my issue and wanted to share it. If someone has an elegant solution that also solves this, I would really love to hear/see it.

Constants

I keep my code separated into two main folders in my lib folder in the project. The “backend” Ecto code in an “api” folder and the “frontend” Absinthe code in a “web” folder.

I wanted to be able to define my enums in a single location. I created a “constants” folder within the “api” folder. This folder holds all my constants throughout the app. A file called enums.ex holds the enum values.

defmodule CoolApp.Constants.Enums do
 @moduledoc """
 The Enums constants are where all enum values should be defined.
 """

 defmacro __using__(_opts) do
   quote do
     @permissions_const [:admin, :guest, :manager, :user]
   end
 end
end

This allows me to “use” the enums within a module simply by referencing @permissions_const.

Enums Macros

In the “api” folder, I created a file called enums.ex

defmodule CoolApp.Enums do
 @moduledoc """
 The Enum provides a location for all enum related macros.
 """
 use CoolApp.Constants.Enums

 defmacro permissions_const, do: Macro.expand(@permissions_const, __CALLER__)
end

This simply creates a macro that returns the enum. Any other enums would be handled the same way and in the same file by creating more macros.

Ecto Enums

In the “api” folder, I create another file called ecto_enums.ex

# Define all enums specifically for Ecto.

import EctoEnum

require CoolApp.Enums

defenum(
 CoolApp.PermissionsEnum,
 :permissions_enum,
 CoolApp.Enums.permissions_const()
)

This is the Ecto syntax for defining enums. It is different than is required in Absinthe.

And using it, the Ecto schema looks like this (note the “name” field):

defmodule CoolApp.Permissions.Permission do
 use Ecto.Schema

 import Ecto.Changeset

 alias CoolApp.{PermissionsEnum, Repo}

 schema "permissions" do
   field(:guid, Ecto.UUID)
   # Use the Permissions Enum.
   field(:name, PermissionsEnum)

   # Creates columns for inserted_at and updated_at timestamps.
   timestamps(type: :utc_datetime_usec)
 end

 @doc false
 def changeset(permission, attrs) do
   permission
   |> Map.merge(attrs)
   |> cast(attrs, [:guid, :name])
   |> validate_required([:guid, :name])
 end

 def data() do
   Dataloader.Ecto.new(Repo, query: &query/2)
 end

 def query(queryable, _params) do
   queryable
 end
end

Database

I also wanted to define the enums in the database. In my migrations (priv -> repo -> migrations) I created a migration that should be executed first. So I named it in a way that always keeps it at the top of the list, 000_enums.exs

defmodule CoolApp.Repo.Migrations.EnableCitextExtension do
 use Ecto.Migration

 alias CoolApp.{PermissionsEnum}

 def up do
   PermissionsEnum.create_type()
 end

 def down do
   PermissionsEnum.drop_type()
 end
end

This will create the enums in the DB. It can then be used in tables like (note the “add :name”):

defmodule CoolApp.Repo.Migrations.CreatePermissions do
 use Ecto.Migration

 def change do
   create table(:permissions) do
     add :name, :permissions_enum
   end

   alter table(:users) do
     add :permission_id, references(:permissions, on_delete: :nothing)
   end

   create index(:users, [:permission_id])
 end
end

Absinthe Enums

Now Absinthe needs the enums. In my “web” folder I have the schema.ex file. I am defining the permissions enum for Absinthe there.

defmodule CoolApp.Schema do
 use Absinthe.Schema

 require CoolApp.Enums

 alias CoolApp.Permissions.Permission

 import_types(CoolApp.Schema.{PermissionTypes, PersonTypes})

 # Define all enums specifically for Absinthe.
 @desc "The selected permission types"
 enum(:permissions_enum, values: CoolApp.Enums.permissions_const())

 def context(ctx) do
   loader =
     Dataloader.new()
     |> Dataloader.add_source(Permission, Permission.data())

   Map.put(ctx, :loader, loader)
 End

 def plugins do
   [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
 End

 query do
   import_fields(:people_queries)
 end
 mutation do
   import_fields(:people_mutations)
 end
end

Now I can use the enum in the Absinthe schema for permissions:

defmodule CoolApp.Schema.PermissionTypes do
 use Absinthe.Schema.Notation

 object :permission do
   field :id, non_null(:id)
   field :name, non_null(:permissions_enum)
 end
end

Frontend Typing

Now the frontend can query GraphQL for types and it will get this:

type Permission {
   id: ID!
   name: PermissionsEnum!
}

"""
The selected permission types
"""
enum PermissionsEnum {
   ADMIN
   EMPLOYEE
   GUEST
   MANAGER
}

The enums are used everywhere but only maintained in a single location!

How can we help?

Can we help you apply these ideas on your project? Send us a message! You'll get to talk with our awesome delivery team on your very first call.