View Source Absinthe.Phoenix.Controller behaviour (absinthe_phoenix v2.0.3)
Supports use of GraphQL documents inside Phoenix controllers.
Example
First, use Absinthe.Phoenix.Controller
, passing your schema
and
notifying Absinthe to operate in internal
mode:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
use Absinthe.Phoenix.Controller, schema: MyAppWeb.Schema, action: [mode: :internal]
# ... actions
end
For each action you want Absinthe to process, provide a GraphQL document using
the @graphql
module attribute (before the action):
@graphql """
query ($filter: UserFilter) {
users(filter: $filter, limit: 10)
}
"""
def index(conn, %{data: data}) do
render conn, "index.html", data
end
The params for the action will be intercepted by the
Absinthe.Phoenix.Controller.Action
plug, and used as variables for
the GraphQL document you've specified.
For instance, given a definition for a :user_filter
input object
type like this:
input_object :user_filter do
field :name_matches, :string
field :age_above, :integer
field :age_below, :integer
end
And a query that looks like this (assuming you have the normal
Plug.Parsers
configuration for param parsing):
?filter[name_matches]=joe&filter[age_above]=42
Then Absinthe will receive variable definitions of:
%{"filter" => %{"name_matches" => "joe", "age_above" => 42}}
(For how the string "42"
was converted into 42
, see cast_param/3
).
The params on the conn
will then be replaced by the result of the
execution by Absinthe. The action function can then match against
that result to respond correctly to the user:
It's up to you to handle the three possible results:
- When there's
:data
but no:errors
, everything went perfectly. - When there's
:errors
but no:data
, a validation error occurred and the document could not be executed. - When there's
:data
and:errors
, partial data is available but some fields reported errors during execution.
Notice the keys are atoms, not strings as in normal Phoenix action invocations.
Differences with the GraphQL Specification
There are some important differences between GraphQL documents as processed in an HTTP API and the GraphQL documents that this module supports.
In an effort to make use of GraphQL ergonomic in Phoenix controllers
and views, Absinthe supports some slight structural modifications to
the GraphQL documents provided using the @graphql
module attribute
in controller modules.
In a way, you can think of these changes as a specialized GraphQL dialect. The following are the differences you need to keep in mind.
Objects can be leaf nodes
Let's look at the users
example mentioned before:
@graphql """
query ($filter: UserFilter) {
users(filter: $filter, limit: 10)
}
"""
You'll notice that in the above example, users
doesn't have an
accompanying selection set (that is, a set of child fields bounded
by { ... }
). The GraphQL specification dictates that only scalar
values can be "leaf nodes" in a GraphQL document... but to support
unmodified struct values being returned (for example, Ecto schemas),
if no selection set is provided for an object value (or list
thereof), the entire value is returned.
The template can then use users
as needed:
<ul>
<%= for user <- @users do %>
<li><%= link user.full_name, to: user_path(@conn, :show, user) %></li>
<% end %>
</ul>
This is useful for Phoenix.HTML
helper functions that expect
structs with specific fields (especially form_for
).
One way to think of this change is that, for objects, no selection set is equivalent to a "splat" operator (except, of course, even fields not defined in your GraphQL schema are returned as part of the value).
But, never fear, nothing is stopping you from ignoring this behavior and providing a selection set if you want a traditionally narrow set of fields:
@graphql """
query ($filter: UserFilter) {
users(filter: $filter, limit: 10) {
id
full_name
}
}
"""
Scalar values aren't serialized
To remove the need for reparsing values, scalar values aren't serialized; Phoenix actions receive the original, unserialized values of GraphQL fields.
This is especially useful for custom scalar types. Using a couple of the
additional types packaged in Absinthe.Type.Custom
, for example:
:decimal
values are returned as%Decimal{}
structs, not strings.:datetime
values are returned as%DateTime{}
structs, not strings.
In short, GraphQL used in controllers is a query language to retrieve the values requested---there's no need to serialize the values to send them across HTTP.
Fields use snake_case
Unlike in the GraphQL notation scheme we prefer for GraphQL APIs (that is,
camelCase
fields, which better match up with the expectations of JavaScript
clients), fields used in documents provided as @graphql
should use
snake_case
naming, as Elixir conventions use that notation style for atoms,
etc.
Atom keys
Because you are writing the GraphQL document in your controller and Absinthe is validating the document against your schema, atom keys are returned for field names.
Summary
Callbacks
Customize the Absinthe processing pipeline.
Cast string param values to values Absinthe expects for variable input.
Callbacks
@callback absinthe_pipeline(schema :: Absinthe.Schema.t(), Keyword.t()) :: Absinthe.Pipeline.t()
Customize the Absinthe processing pipeline.
Only implement this function if you need to change the pipeline used to process documents.
@callback cast_param( value :: any(), target_type :: Absinthe.Type.t(), schema :: Absinthe.Schema.t() ) :: any()
Cast string param values to values Absinthe expects for variable input.
Some scalar types, like :integer
(GraphQL Int
) require that raw,
incoming value be a non-string type. This isn't a problem in
GraphQL-over-HTTP because the variable values are provided as a JSON
payload (which supports, i.e., integer values).
To support converting incoming param values to the format that
certain scalars expect, we support a cast_param/3
callback
function that takes a raw value, target type (e.g., the scalar
type), and the schema, and returns the transformed
value. cast_param/3
is overridable and the implementation already
supports :integer
and :float
types.
If you override cast_param/3
, make sure you super or handle lists,
non-nulls, and input object values yourself; they're also processed
using the function.
Important: In the event that a value is invalid, just return it unchanged so that Absinthe's usual validation logic can report it as invalid.