Bruce Williams
Twitter: @wbruce (DMs open)
GitHub: @bruce
Elixir Forum: @bruce
Elixir Slack: Bruce Williams
v1.4
query {
post(id: "abc-123") {
title
publishedDate
author {
name
}
body
}
}
{
"data": {
"post": {
"title": "CodeBEAM SF",
"publishedDate": "2018-03-19",
"author": {
"name": "Bruce Williams"
},
"body": "..."
}
}
}
query ($userId: ID!, $since: Date) {
user(id: $userId) {
posts(since: $since) {
title
publishDate
}
}
}
{"userId":"12", "since":"2018-01-01"}
{
"data": {
"user": {
"posts":[
{"title": "Example1", "publishDate":"2018-01-01"},
{"title": "Example2", "publishDate":"2018-01-02"}
]
}
}
}
Subscribes to an event,
requests information be
sent later (over WS, etc)
query ($search: String, $limit: Int = 10) {
posts(matching: $search, limit: $limit) {
title
author { name }
}
}
Requests information.
mutation ($postContent: PostInput!) {
createPost(input: $postContent) {
assignedEditor { name }
publicationStatus
}
}
subscription ($postId: ID!) {
reviewCompleted(postId: $postId) {
editor { name }
requestedChanges {
changeType
notes
}
}
}
Modifies data, requests
information in response.
Post
Comment
User
posts
author
comments
subject
String
Date
name
birthdate
Object types
, fields
, and extensible scalar types
... plus enumerations, unions, interfaces, and others
Post
User
posts
Resolver
w/ defined arguments
Post
User
posts
Query
Root
posts
post
user
users
Root object types are entry points for GraphQL operations
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
This is a query operation, resolve the root query value.
Resolve its posts field, given the provided matching and limit arguments. Expect a list of values we can treat as Post object typed as the result.
For each Post typed value...
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
Resolve its title field. Expect a value that we can handle as a String scalar type as the result. Save it.
Resolve the post's author field. Expect a value we can treat as a User object type as the result.
With that User value...
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
Resolve the name field. Expect a value that we can treat as a String scalar type as the result. Save it.
No more fields for the User typed value. Go back up a level to the last Post typed value.
Resolve its comments field, given the provided input values for the limit and order arguments. Expect a list of values we can treat as Comment typed as the result.
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
Resolve the body field. Expect a value we can treat as a String scalar type as the result. Save it.
No more fields for the Comment typed value, go back up a level to the last Post typed value.
For each Comment typed value...
No more fields for the Post typed value, go back up a level to the root query type value.
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
No more fields for the root query type value, collect the saved data and return it as a tree data structure.
Standard installation; in your mix.exs:
defp deps do
[
# As expected...
{:absinthe, "~> 1.4.0"},
# If you want to use HTTP (likely!)
{:absinthe_plug, "~> 1.4.0"},
# Rest of deps...
]
end
A module, wherever you want to put it:
defmodule MyAppWeb.Schema do
use Absinthe.Schema
# More on this later...
end
If you only care about application/graphql requests:
plug Absinthe.Plug,
schema: MyAppWeb.Schema
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
json_decoder: Poison
plug Absinthe.Plug,
schema: MyAppWeb.Schema
This is the more standard approach, supporting application/json:
If you only care about GraphQL requests, add to your Endpoint:
defmodule MyApp.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug Plug.RequestId
plug Plug.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Poison
plug Absinthe.Plug,
schema: MyAppWeb.Schema
end
... and remove your Router.
If you only want GraphQL on a particular route, add to your Router:
defmodule MyAppWeb.Router do
use Phoenix.Router
resource "/pages", MyAppWeb.PagesController
forward "/api", Absinthe.Plug,
schema: MyAppWeb.Schema
end
Add to your mix.exs:
def deps do
[
# Others...
{:absinthe_phoenix, "~> 1.4.0"}
]
end
[
# Other children ...
supervisor(MyAppWeb.Endpoint, []),
supervisor(Absinthe.Subscription, [MyAppWeb.Endpoint]),
# Other children ...
]
Add this to your Application:
Add to your Endpoint:
use Absinthe.Phoenix.Endpoint
Add this to your socket, for example, UserSocket:
use Absinthe.Phoenix.Socket,
schema: MyAppWeb.Schema
Authentication/authorization concerns can be handled in connect/2.
For JavaScript clients, see:
https://github.com/absinthe-graphql/absinthe-socket
Standard JS client + drop-in support for the Relay and Apollo client-side GraphQL frameworks.
Thank you, Christian and Mauro!
Post
User
posts
author
defmodule MyAppWeb.Schema do
use Absinthe.Schema
# Rest of schema...
object :post do
field :title, :string
field :publish_date, :date
field :author, :user
field :body, :string
end
object :user do
field :name, :string
field :posts, list_of(:post)
end
end
object :user do
field :name, :string
field :posts, list_of(:post), resolve: &find_posts/3
end
def find_posts(user, _args, _info) do
posts = Repo.all(Ecto.assoc(user, :posts))
{:ok, posts}
end
defmodule MyAppWeb.Context do
@behaviour Plug
import Plug.Conn
def init(opts), do: opts
def call(conn, _) do
context = build_context(conn)
Absinthe.Plug.put_options(conn, context: context)
end
@doc """
Return the current user context based on the authorization header
"""
def build_context(conn) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, current_user} <- authorize(token) do
%{current_user: current_user}
else
_ -> %{}
end
end
defp authorize(token) do
# Authorization logic...
end
end
If you're just using Plug:
plug MyAppWeb.Context
plug Absinthe.Plug,
schema: MyAppWeb.Schema
pipeline :graphql do
plug MyAppWeb.Context
end
scope "/api" do
pipe_through :graphql
forward "/", Absinthe.Plug,
schema: MyAppWeb.Schema
end
If you're using Phoenix, then use a pipeline in your Router:
In a structural query:
query do
field :me, :user do
resolve fn
_, _, %{context: context} ->
{:ok, Map.get(context, :current_user)}
end
end
end
object :user do
@desc "Get the user's posts"
field :posts, list_of(:post), resolve: dataloader(Post)
end
query {
me {
posts {
title
}
}
}
In an ad hoc check:
query do
@desc "Get the full list of authors"
field :authors, list_of(:authors) do
resolve fn
_, _, %{context: %{current_user: %{admin: true}}} ->
{:ok, Repo.all(User)}
_, _, _ ->
{:error, "Unauthorized"}
end
end
end
query {
authors {
name
}
}
Writing middleware:
defmodule MyAppWeb.Middleware.AdminRequired do
@behaviour Absinthe.Middleware
def call(resolution, _) do
case resolution.context do
%{current_user: %{admin: true}} ->
resolution
_ ->
Absinthe.Resolution.put_result(
resolution,
{:error, "Unauthorized"}
)
end
end
end
end
Applying it ad hoc, per field:
field :protected_thing, :thing do
middleware MyAppWeb.Middleware.AdminRequired
resolve &resolve_thing/3
end
or, applying it using middleware/3:
def middleware(middleware, %{identifier: :protected_thing} = _field, _object) do
[MyAppWeb.Middleware.AdminRequired | middleware]
end
def middleware(middleware, _field, _object), do: middleware
field :time_consuming, :thing do
resolve fn _, _, _ ->
async(fn ->
{:ok, long_time_consuming_function()}
end)
end
end
Uses Task.async under the hood (with a default timeout of 30s).
Absinthe.Middleware.Async also serves as an excellent example of how to build middleware.
object :post do
field :name, :string
field :author, :user do
resolve fn post, _, _ ->
batch({__MODULE__, :users_by_id}, post.author_id, fn batch_results ->
{:ok, Map.get(batch_results, post.author_id)}
end)
end
end
end
def users_by_id(_, user_ids) do
users = Repo.all from u in User, where: u.id in ^user_ids
Map.new(users, fn user -> {user.id, user} end)
end
def deps do
[
{:dataloader, "~> 1.0.0"},
# ...
]
end
Add the dependency to your mix.exs:
defmodule MyApp.Blog
def data do
# Can customize how queries are built by
# passing a function
Dataloader.Ecto.new(MyApp.Repo)
end
end
Setup the dataloader source (usually in your Phoenix bounded context):
object :post do
field :name, :string
field :author, :user, resolve: dataloader(Blog)
end
Use it in your schema:
defmodule MyAppWeb.Schema.PublishingTypes do
use Absinthe.Schema.Notation
object :post do
field :title, :string
field :publish_date, :date
field :author, :user
field :body, :string
end
object :post_queries do
field :posts, list_of(:posts)
# Other fields for root query type
end
# Other types...
end
defmodule MyAppWeb.Schema do
use Absinthe.Schema
import_types MyAppWeb.Schema.{
PublishingTypes,
AccountTypes
}
query do
import_fields :post_queries
import_fields :user_queries
end
end
v1.5
NEXT:
@wbruce
http://absinthe-graphql.org
https://pragprog.com/book/wwgraphql
20% off (until Sun):
CodeBEAM SF 2018