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!
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.