Current section

Files

Jump to
graphql_relay lib graphql relay connection ecto.ex
Raw

lib/graphql/relay/connection/ecto.ex

if Code.ensure_loaded?(Ecto) do
defmodule GraphQL.Relay.Connection.Ecto do
@moduledoc """
Interface between Relay Connections and Ecto. This module allows you to
back a Relay Connection with an Ecto query.
## Example
A Relay connection starts with a connection definition. For example, a
`user` can have many `todos` (see code comments):
```elixir
%ObjectType{
name: "User",
description: "The user",
fields: %{
id: Node.global_id_field("user"),
name: %{type: %String{}},
email: %{type: %String{}},
todos: %{
type: Todo.connection[:connection_type],
description: "The todos this user owns",
args: Map.merge(
%{status: %{type: %String{}, defaultValue: "any"}},
Connection.args
),
resolve: fn(user, args, _ctx) ->
# Here we prepare our Ecto query
query = Ecto.assoc(user, :todos)
query = case args do
%{status: "active"} -> from things in query, where: things.complete == false
%{status: "completed"} -> from things in query, where: things.complete == true
_ -> query
end
# Here we resolve the connection using our Ecto Connection module,
# passing it the Ecto Repo we want to query. The `resolve/3` function
# will execute the query and return the results in the form Relay
# requires.
Connection.Ecto.resolve(Repo, query, args)
end
},
},
interfaces: [Root.node_interface]
}
```
For a full example see https://github.com/graphql-elixir/graphql_relay/blob/master/examples/todo/web/graphql/user.ex
"""
import Ecto.Query
@prefix "ectoconnection:"
@doc """
WARNING: this function signature is deprecated. Use `resolve/3`.
"""
def resolve(query, %{repo: repo} = args) do
# Emit deprecation warning once we hit >= v0.6 per RELEASE.md#Deprecations
# :elixir_errors.warn __ENV__.line, __ENV__.file, "Use of `GraphQL.Relay.Connection.Ecto.resolve/2` is deprecated! Use `GraphQL.Relay.Connection.Ecto.resolve/3`."
resolve(repo, query, args)
end
def resolve(repo, query, args \\ %{}) do
before = cursor_to_offset(args[:before])
# `after` is a keyword http://elixir-lang.org/docs/master/elixir/Kernel.SpecialForms.html#try/1
a_after = cursor_to_offset(args[:after])
first = args[:first]
last = args[:last]
limit = Enum.min([first, last, connection_count(repo, query)])
query = if a_after do
from things in query, where: things.id > ^a_after
else
query
end
query = if before do
from things in query, where: things.id < ^before
else
query
end
# Calculate has_next_page/has_prev_page before order_by to avoid group_by requirement
has_next_page = case first do
nil -> false
_ ->
first_limit = first + 1
has_more_records_query = make_query_countable(from things in query, limit: ^first_limit)
has_more_records_query = from things in has_more_records_query, select: count(things.id)
repo.one(has_more_records_query) > first
end
has_prev_page = case last do
nil -> false
_ ->
last_limit = last + 1
has_prev_records_query = make_query_countable(from things in query, limit: ^last_limit)
has_prev_records_query = from things in has_prev_records_query, select: count(things.id)
repo.one(has_prev_records_query) > last
end
query = if first do
from things in query, order_by: [asc: things.id], limit: ^limit
else
query
end
query = if last do
from things in query, order_by: [desc: things.id], limit: ^limit
else
query
end
records = repo.all(query)
edges = Enum.map(records, fn(record) ->
%{
cursor: cursor_for_object_in_connection(record),
node: record
}
end)
edges = case last do
nil -> edges
_ -> Enum.reverse(edges)
end
first_edge = List.first(edges)
last_edge = List.last(edges)
%{
edges: edges,
pageInfo: %{
startCursor: first_edge && Map.get(first_edge, :cursor),
endCursor: last_edge && Map.get(last_edge, :cursor),
hasPreviousPage: has_prev_page,
hasNextPage: has_next_page
}
}
end
def cursor_to_offset(nil), do: nil
def cursor_to_offset(cursor) do
case Base.decode64(cursor) do
{:ok, decoded_cursor} ->
{int, _} = Integer.parse(String.slice(decoded_cursor, String.length(@prefix)..String.length(decoded_cursor)))
int
:error ->
nil
end
end
def cursor_for_object_in_connection(object) do
Base.encode64("#{@prefix}#{object.id}")
end
def connection_count(repo, query) do
query = make_query_countable(query)
count_query = from things in query, select: count(things.id)
repo.one(count_query)
end
defp make_query_countable(query) do
query
|> remove_order
|> remove_preload
|> remove_select
end
# Remove order by if it exists so that we avoid `field X in "order_by"
# does not exist in the model source in query`
defp remove_order(query) do
Ecto.Query.exclude(query, :order_by)
end
# Remove preload if it exists so that we avoid "the binding used in `from`
# must be selected in `select` when using `preload` in query`"
defp remove_preload(query) do
Ecto.Query.exclude(query, :preload)
end
# Remove select if it exists so that we avoid `only one select
# expression is allowed in query` Ecto exception
defp remove_select(query) do
Ecto.Query.exclude(query, :select)
end
end
else
defmodule GraphQL.Relay.Connection.Ecto do
def resolve(_, _) do
raise RuntimeError, message: """
Ecto not available. Make sure Ecto is installed as a dependency of your
application.
"""
end
end
end