Skip to content

Instantly share code, notes, and snippets.

@muziyoshiz
Created March 21, 2017 10:27
Show Gist options
  • Save muziyoshiz/47ff80c7b4c96b8e9432e100b00b5c70 to your computer and use it in GitHub Desktop.
Save muziyoshiz/47ff80c7b4c96b8e9432e100b00b5c70 to your computer and use it in GitHub Desktop.
Sample of GraphQL Relay server on Phoenix
# mix.exs
defmodule PhoenixRelaySample.Mixfile do
use Mix.Project
def project do
[app: :phoenix_relay_sample,
version: "0.0.1",
elixir: "~> 1.2",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
aliases: aliases(),
deps: deps()]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[mod: {PhoenixRelaySample, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :absinthe_plug, :absinthe_relay]]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
defp elixirc_paths(_), do: ["lib", "web"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.2.1"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:absinthe_plug, "~> 1.2.0"},
{:absinthe_relay, "~> 1.2.0"},
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
"test": ["ecto.create --quiet", "ecto.migrate", "test"]]
end
end
# web/router.ex
defmodule PhoenixRelaySample.Router do
use PhoenixRelaySample.Web, :router
# pipeline :browser do
# plug :accepts, ["html"]
# plug :fetch_session
# plug :fetch_flash
# plug :protect_from_forgery
# plug :put_secure_browser_headers
# end
#
# pipeline :api do
# plug :accepts, ["json"]
# end
#
# scope "/", PhoenixRelaySample do
# pipe_through :browser # Use the default browser stack
#
# get "/", PageController, :index
# end
# Other scopes may use custom stacks.
# scope "/api", PhoenixRelaySample do
# pipe_through :api
# end
scope "/" do
if Mix.env == :dev do
forward "/graphql", Absinthe.Plug.GraphiQL, schema: PhoenixRelaySample.Schema
else
forward "/graphql", Absinthe.Plug, schema: PhoenixRelaySample.Schema
end
end
end
# web/schema.ex
defmodule PhoenixRelaySample.Schema do
use Absinthe.Schema
use Absinthe.Relay.Schema
alias PhoenixRelaySample.UserResolver
alias PhoenixRelaySample.User
import_types PhoenixRelaySample.Schema.Types
query do
@desc """
A list of users.
"""
connection field :users, node_type: :user do
# Argument `first`, `last`, `before` and `after` are automatically added
resolve &UserResolver.list/3
end
@desc """
Fetches an object given its ID.
"""
node field do
resolve fn
parent, %{type: :user, id: id}, _ ->
UserResolver.find(parent, %{id: id}, %{})
end
end
@desc """
Hack to workaround https://github.com/facebook/relay/issues/112 re-exposing the root query object
"""
field :relay, :query do
resolve (fn _parent, _arg, _info -> {:ok, %{}} end)
end
end
@desc """
An object with an ID.
"""
node interface do
resolve_type fn
%User{}, _ ->
:user
_, _ ->
nil
end
end
end
# web/schema/type.ex
defmodule PhoenixRelaySample.Schema.Types do
use Absinthe.Schema.Notation
use Absinthe.Relay.Schema.Notation
@desc "A user."
node object :user do
@desc "The user's name."
field :name, non_null(:string)
@desc "The user's gender."
field :gender, :string
@desc "The user's age."
field :age, :integer
@desc "The company the user works at."
field :company, :company do
resolve &resolve_company/3
end
end
@desc "A company."
object :company do
@desc "The company's name."
field :name, :string
end
connection node_type: :user
defp resolve_company(parent, _args, _info) do
case parent do
%{company_name: company_name} ->
{:ok, %{name: company_name}}
_ ->
{:ok, nil}
end
end
end
# web/models/user.ex
defmodule PhoenixRelaySample.User do
use PhoenixRelaySample.Web, :model
schema "users" do
field :name, :string
field :gender, :string
field :age, :integer
field :company_name, :string
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :gender, :age, :company_name])
|> validate_required([:name])
end
end
# web/resolvers/user_resolver.ex
defmodule PhoenixRelaySample.UserResolver do
alias PhoenixRelaySample.User
alias PhoenixRelaySample.Repo
import Ecto.Query
def find(_parent, %{id: id}, _info) do
case Repo.get(User, id) do
nil -> {:error, "Not found"}
user -> {:ok, user}
end
end
def list(_parent, args, _info) do
# Error messages same as that of GitHub GraphQL API
case args do
%{first: _first, last: _last} ->
{:error, "Passing both `first` and `last` values to paginate the `users` connection is not supported."}
%{first: _first, before: _before} ->
{:error, "Passing `first` with `before` is not supported."}
%{first: _first} ->
do_list(args)
%{last: _last, after: _after} ->
{:error, "Passing `last` with `after` is not supported."}
%{last: _last} ->
do_list(args)
_ ->
{:error, "You must provide a `first` or `last` value to properly paginate the `users` connection."}
end
end
defp do_list(args) do
# You must supply a count if using `last` without `before`
count = Repo.aggregate(User, :count, :id)
# args in from_query(query, repo_fun, args, opts \\ []) must have args[:first] or args[:last].
# The sorting is required before from_query.
conn =
User
|> order_by(asc: :id)
|> Absinthe.Relay.Connection.from_query(&Repo.all/1, args, [count: count])
{:ok, conn}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment