Thomas Citharel

Salut, je suis Thomas Citharel, un être humain avec des compétences de développement web.

Dynamically add enum values in Elixir's GraphQL Library Absinthe

Do you have a huge list of value acceptable for a GraphQL enum? Absinthe can dynamically extend the schema at runtime.

While adding a "category" feature to the events in Mobilizon, I wanted to restict the values this field could take. Usually I use an enum in the GraphQL schema definition for this kind of situation, for instance:

@desc "The list of join options for an event"
enum :event_join_options do
value(:free, description: "Anyone can join and is automatically accepted")
value(:restricted, description: "Manual acceptation")
value(:invite, description: "Participants must be invited")
end

This uses the enum macro and three value macros. But in this case there would be a lot of options for value, so it wouldn't make sense to have them all in the schema declaration.

Moreover, I want this list of categories to be extendable in the future by the instance administrator, who doesn't want to recompile to extend the list.

So the Absinthe docs are far from being perfect, but you can find mention of custom schema manipulation. This page, with the help of a few Github issues and a test file, helped me see clear through this.

For starters, let's create a new module to dynamically build our new enum. There's a few comments inline.

defmodule Mobilizon.GraphQL.Schema.Custom.EnumTypes do
alias Absinthe.Blueprint.Schema
alias Absinthe.Schema.Notation
alias Absinthe.{Blueprint, Pipeline, Phase}

# The list of categories
# having an atom identifier and a string label
# This is currently being defined as a module attribute,
# but in the future it will be expanded by some admin
# configuration as well
@categories [
%{
id: :arts,
label: "ARTS"
},
# … a long list of categories
]

def pipeline(pipeline) do
# When to insert this override
Pipeline.insert_after(pipeline, Phase.Schema.TypeImports, __MODULE__)
end

# Adding this enum to the list of schema definitions
def run(blueprint = %Blueprint{}, _) do
%{schema_definitions: [schema]} = blueprint

new_enum = build_dynamic_enum()

schema =
Map.update!(schema, :type_definitions, fn type_definitions ->
[new_enum | type_definitions]
end)

{:ok, %{blueprint | schema_definitions: [schema]}}
end

# Building the enum itself
def build_dynamic_enum do
%Schema.EnumTypeDefinition{
name: "EventCategory",
identifier: :event_category,
module: __MODULE__,
__reference__: Notation.build_reference(__ENV__),
values:
# Create a `Schema.EnumValueDefinition`
# for each value in `@categories`
Enum.map(@categories, fn %{id: id, label: label} ->
%Schema.EnumValueDefinition{
identifier: id,
# The `value` can either be `id` or `label`
# depending whether what you want to be given
value: label,
name: label,
module: __MODULE__,
__reference__: Notation.build_reference(__ENV__)
}
end)
}
end
end

Finally, we just have to add in our schema definition file the following @pipeline_modifier attribute:

defmodule Mobilizon.GraphQL.Schema do
use Absinthe.Schema

@pipeline_modifier Custom.EnumTypes

# [… our whole schema]
end

And boom, we can use our new enum just like this:

field :create_event, type: :event do
# …
arg(:category,
:event_category,
# string or atom depending on what you use for value above
default_value: "ARTS",
description: "The event's category"
)
# …
end

Previous

Imprimer en PDF