Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save cheerfulstoic/4f676f0b5de42f3f5f60d06ae03c4dfc to your computer and use it in GitHub Desktop.
Save cheerfulstoic/4f676f0b5de42f3f5f60d06ae03c4dfc to your computer and use it in GitHub Desktop.
Middleware for Elixir's `absinthe` to deal with efficient querying of `edges` and `totalCount`
defmodule MyAppWeb.Graphql.Middleware.RelayConnectionLazyEvaluate do
@moduledoc """
Absinthe requires all of the data for a connection to be loaded in the `resolve`
for the field. This means that regardless of if a client requests `edges` or
`totalCount`, the server needs to do one query for both every time.
This middleware evaluates the query that was done by the client to figure out
what fields were requested. It will only load the data which is required for
a typical connection based on what the client has requested
As a nice side-effect, since we only run Connection.from_query when
`edges` has been requested, we don't require it for fields like `totalCount`
"""
defmodule Value do
@moduledoc """
Holds the information needed to query for Relay connections
"""
defstruct ~w[query args total_count]a
end
@behaviour Absinthe.Middleware
use OK.Pipe
@impl Absinthe.Middleware
def call(%{state: :resolved, errors: errors, value: %Value{} = value} = resolution, _config) do
# It would be nice if we were given "total_count" instead of "totalCount".
{new_value, new_errors} =
resolution.definition.selections
|> Enum.map(& &1.name)
|> Enum.reduce({%{}, []}, fn
"edges", {result, errors} ->
case Absinthe.Relay.Connection.from_query(value.query, &MyApp.Repo.all/1, value.args) do
{:ok, value} -> {Map.merge(result, value), errors}
{:error, message} -> {result, [message | errors]}
end
"totalCount", {result, errors} ->
{Map.put(
result,
:total_count,
value.total_count || MyApp.Repo.aggregate(value.query, :count)
), errors}
_, {result, errors} ->
{result, errors}
end)
resolution
|> Map.put(:value, new_value)
|> Map.put(:errors, errors ++ new_errors)
end
def call(resolution, _config), do: resolution
end
connection field :followers, node_type: :user do
resolve(fn args, %{source: page, context: context} ->
query = Pages.followers_query(page)
# Custom logic to deal with filtering removed
{:ok, %RelayConnectionLazyEvaluate.Value{query: query, args: args}}
end)
end
def middleware(middleware, %Absinthe.Type.Field{type: field_type}, _object)
when is_atom(field_type) do
is_connection =
field_type
|> Atom.to_string()
|> String.match?(~r/_connection\Z/)
if is_connection do
middleware ++ [MyAppWeb.Graphql.Middleware.RelayConnectionLazyEvaluate]
else
middleware
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment