A

GraphQL in Elixir

Primer

Bruce Williams

Bruce williams

Twitter: @wbruce (DMs open)

GitHub: @bruce

Elixir Forum: @bruce

Elixir Slack: Bruce Williams

industry 1

INDUSTRY 2

INDUSTRY 3

co-Creator

EST. Oct 2015

v1.4

Absinthe

co-author

beta now, print soon

Ben Wilson

tempus

FUGIT

agenda

  • Basic background on GraphQL, how it works, some benefits, and how you might choose to employ it.
  • Getting started with Absinthe, the GraphQL toolkit for Elixir, some handy glossary words, and answers to some frequently asked questions.

GraphQL

Basics

Graph

Query

Language

data query language

compare to: sql

query {
  post(id: "abc-123") {
    title
    publishedDate
    author {
      name
    }
    body
  }
}
{
  "data": {
    "post": {
      "title": "CodeBEAM SF",
      "publishedDate": "2018-03-19",
      "author": {
        "name": "Bruce Williams"
      },
      "body": "..."
    }
  }
}

you get what you ask for

http api

compare to: rest

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"}
      ]
    }
  }
}

GraphQL

variables

result

option #1

replace rest

Database

graphql api

option #2

aggregate services

rest API

graphql api

rest API

etc

option #3

abstract data access

database

REST api

graphql (directly)

option #?

mix and match

handy Elixir Adoption Idea

it's not just about queries

query

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 }
  }
}

mutation

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.

subscription

GraphQL Operations

websocket!

how it works

schema

a graph of relationships between types

Post

Comment

User

posts

author

comments

subject

String

Date

name

birthdate

Object types

, fields

, and extensible scalar types

... plus enumerations, unions, interfaces, and others

Defining a GraphQL Schema

Post

User

posts

Defining a GraphQL Schema

Resolver

w/ defined arguments

Post

User

posts

Query

Root

posts

post

user

users

Root object types are entry points for GraphQL operations

Defining a GraphQL Schema

graphql document

describes a path to walk through a schema graph

a graphQL query narrative

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

a graphQL query narrative

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

a graphQL query narrative

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.

a graphQL query narrative

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.

a graphQL query narrative

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.

introspection

tools

USING ABSINTHE

installation

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

define a schema

A module, wherever you want to put it:

defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  # More on this later...
end

setup for the web minimalist

using with plug

setup with plug

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:

SETUP for the WEB pragmatist

using with phoenix

PHOENIX SETUP

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.

PHOENIX SETUP

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

standard subscriptions setup

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:

standard subscriptions setup

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.

standard subscriptions setup

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!

define schemas

using elixir macros

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

Basic Resolver

standard graphql

VALIDATION

PARSING

EXECUTION

ABSINTHE

PHASES

40+

frequently

asked

questions

how do you handle authorization?

Q:

populate the absinthe context with the current user, then use/check that user. middleware helps!

A:

auth: writing the CONTEXT plug

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

AUTH: wiring in the CONTEXT plug

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:

AUTH: using the current user

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
    }
  }
}

AUTH: using the current user

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
  }
}

AUTH: using the current user

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

AUTH: using the current user

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

how do you resolve fields concurrently?

Q:

Use the async plugin.

A:

field :time_consuming, :thing do
  resolve fn _, _, _ ->
    async(fn ->
      {:ok, long_time_consuming_function()}
    end)
  end
end

async plugin

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.

how do you batch resolution to avoid N+1 query problems?

Q:

Use the batching plugin (or even better, dataloader).

A:

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

batching plugin

def deps do
  [
    {:dataloader, "~> 1.0.0"},
    # ...
  ]
end

using dataloader

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

using dataloader

Use it in your schema:

how do you break-up a schema module?

Q:

extract types to other modules, then import them.

A:

A bad schema structure can be exhausting.

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

extract types

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

import types & fields

v1.5

NEXT:

refactored schema definition system

new parser + pluggability

schema stitching

graphql SDL support

more flexible imports

elixir graphql client

dataloader improvements

thank you!

@wbruce

http://absinthe-graphql.org

https://pragprog.com/book/wwgraphql

20% off (until Sun):

CodeBEAM SF 2018

A GraphQL in Elixir Primer

By wbruce

A GraphQL in Elixir Primer

My talk for CodeBEAM SF 2018, covering the use of GraphQL in the Elixir programming language.

  • 1,649