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
endFor 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
endThe 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
endAnd a query that looks like this (assuming you have the normal
Plug.Parsers configuration for param parsing):
?filter[name_matches]=joe&filter[age_above]=42Then 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
:databut no:errors, everything went perfectly. - When there's
:errorsbut no:data, a validation error occurred and the document could not be executed. - When there's
:dataand: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:
:decimalvalues are returned as%Decimal{}structs, not strings.:datetimevalues 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.