Created
November 16, 2020 09:40
-
-
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`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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