Skip to content

Instantly share code, notes, and snippets.

@redrabbit
Last active December 1, 2023 14:29
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save redrabbit/be3528a4e4479886acbe648a693e65c0 to your computer and use it in GitHub Desktop.
Save redrabbit/be3528a4e4479886acbe648a693e65c0 to your computer and use it in GitHub Desktop.
Absinthe.Ecto.Resolution.Schema
defmodule Absinthe.Ecto.Resolution.Schema do
@moduledoc """
This module provides helper functions to resolve a GraphQL query into `Ecto.Query`.
"""
import Absinthe.Resolution.Helpers
import Ecto.Query
alias Absinthe.Resolution
alias Absinthe.Blueprint.Document.Field
@doc """
Returns an `Ecto.Query` for the given parameters.
The query is built using the given `Ecto.Schema` and `Absinthe.Resolution` fields.
This function automatically resolves `:joins`, `:preload` and `:select` clauses.
For example, following GraphQL query:
{
artists {
name
albums {
title
tracks {
title
}
genres {
name
}
}
}
Returns a single `Ecto.Query`:
#Ecto.Query<from a0 in Artist,
join: a1 in assoc(a0, :albums),
join: g in assoc(a1, :genres),
join: t in assoc(a1, :tracks),
select: [:id, :name, {:albums, [:id, :title, {:genres, [:id, :name]}, {:tracks, [:id, :title]}]}],
preload: [albums: {a1, [genres: g, tracks: t]}]>
Required associations are joined an preloaded automatically. Also, not that
only required fields are selected.
"""
@spec build_query(Ecto.Schema.t, Resolution.t, (Ecto.Query.t -> Ecto.Query.t)) :: Ecto.Query.t
def build_query(schema, info, transform \\ nil) do
fields = resolve_fields(info)
select = resolve_fields(schema, fields)
assocs = resolve_assocs(schema, fields)
new_query(schema, select, assocs, transform)
end
@doc """
Returns an `Ecto.Query` for the given batch parameters.
This function is meant to be used to avoid *N+1* queries and resolve
associations using `Absinthe.Resolution.Helpers.batch/4`.
Here's a basic usage example:
def resolve_artist_albums(artist, _args, info) do
batch_ecto_query(Music.Album, artist, info, &Repo.all/1)
end
You can also use `transform` to apply further query functions:
def resolve_artist_top_tracks(artist, args, info) do
batch_query(Music.Track, artist, info, &Repo.all/1, fn query ->
query
|> order_by([desc: :popularity])
|> limit(^Map.get(args, :top, 10))
end)
end
"""
@spec batch_query(
Ecto.Schema.t,
Ecto.Schema.t | {Atom.t, term},
Resolution.t,
function,
(Ecto.Query.t -> Ecto.Query.t)
) :: term
def batch_query(schema, struct, info, execute, transform \\ nil)
def batch_query(schema, {assoc_field, id}, info, execute, transform) do
fields = resolve_fields(info)
select = [assoc_field|resolve_fields(schema, fields)]
assocs = resolve_assocs(schema, fields)
query = new_query(schema, select, assocs, transform)
batch({__MODULE__, :batch_query, {execute, query, assoc_field}}, id, &{:ok, Map.get(&1, id)})
end
def batch_query(right, struct, info, execute, transform) do
left = struct.__struct__
{assoc_field, pk} = Enum.find_value(right.__schema__(:associations), &resolve_assoc_field(left, right, &1))
id = Map.fetch!(struct, pk)
batch_query(left, {assoc_field, id}, info, execute, transform)
end
defp resolve_assoc_field(left, right, assoc) do
case left.__schema__(:association, assoc) do
%Ecto.Association.Has{owner_key: pk, owner: ^left, related: ^right, related_key: assoc_field} ->
{assoc_field, pk}
_else ->
nil
end
end
@doc false
def batch_query({execute, query, assoc_field}, ids) do
query
|> batch_in(assoc_field, ids)
|> execute.()
|> Enum.reduce(%{}, &Map.update(&2, Map.fetch!(&1, assoc_field), [&1], fn l -> [&1|l] end))
end
#
# Helpers
#
defp new_query(schema, select, assocs, transform) do
schema
|> from(select: ^select)
|> transform_query(transform)
|> struct!(joins: join_assocs(assocs), assocs: preload_assocs(assocs))
end
defp transform_query(query, nil), do: query
defp transform_query(query, fun), do: fun.(query)
defp batch_in(query, assoc, [id]), do: where(query, ^[{assoc, id}])
defp batch_in(query, assoc, ids) do
tagged_values =
for id <- ids, do: %Ecto.Query.Tagged{tag: nil, type: {0, assoc}, value: id}
expr = {:in, [], [{{:., [], [{:&, [], [0]}, assoc]}, [], []}, tagged_values]}
struct!(query, wheres: [%Ecto.Query.QueryExpr{expr: expr}])
end
defp join_assocs(assocs) do
assocs
|> resolve_joins()
|> elem(0)
|> Enum.map(&join_assoc/1)
end
defp join_assoc({field, index}) do
%Ecto.Query.JoinExpr{qual: :inner, assoc: {index, field}, on: %Ecto.Query.QueryExpr{expr: true}}
end
defp preload_assocs(assocs, index \\ 1) do
assocs
|> Enum.map_reduce(index, &preload_assoc/2)
|> elem(0)
end
defp preload_assoc(field, index) when is_atom(field) do
{{field, {index, []}}, index + 1}
end
defp preload_assoc({field, assocs}, index) do
{{field, {index, preload_assocs(assocs, index + 1)}}, index + 1}
end
defp resolve_name(%Field{schema_node: %{__reference__: ref}}), do: ref.identifier
defp resolve_fields(%Resolution{definition: %{fields: fields}}), do: fields
defp resolve_fields(schema, fields) when is_list(fields) do
fields
|> Enum.flat_map(&resolve_field(schema, &1))
|> List.insert_at(0, :id)
|> Enum.dedup()
end
defp resolve_field(_schema, %Field{schema_node: %{resolve: resolver}}) when is_function(resolver), do: []
defp resolve_field(_schema, %Field{fields: []} = f), do: List.wrap(resolve_name(f))
defp resolve_field(schema, %Field{fields: fields} = f) do
name = resolve_name(f)
{schema, assoc} = resolve_schema_assoc(schema, name)
field = {name, resolve_fields(schema, fields)}
List.wrap(if assoc, do: [assoc, field], else: field)
end
defp resolve_assocs(schema, fields) when is_list(fields) do
fields
|> Enum.filter(&expandable?/1)
|> Enum.flat_map(&resolve_assoc(schema, &1))
end
defp resolve_assoc(_schema, %Field{schema_node: %{resolve: resolver}}) when is_function(resolver), do: []
defp resolve_assoc(schema, %Field{fields: fields} = f) do
name = resolve_name(f)
meta = resolve_assocs(schema, fields)
List.wrap(if Enum.empty?(meta), do: name, else: {name, meta})
end
defp resolve_joins(assocs, joins \\ [], index \\ 0) do
Enum.reduce(assocs, {joins, index}, &resolve_join/2)
end
defp resolve_join(leaf, {joins, index}) when is_atom(leaf) do
{joins ++ [{leaf, index}], index}
end
defp resolve_join({root, leafs}, {joins, index}) do
joins = joins ++ [{root, index}]
resolve_joins(leafs, joins, index + 1)
end
defp resolve_schema_assoc(schema, name) do
case schema.__schema__(:association, name) do
%Ecto.Association.BelongsTo{owner_key: assoc_field} ->
{schema, assoc_field}
%Ecto.Association.Has{related: schema} ->
{schema, nil}
%Ecto.Association.ManyToMany{related: schema} ->
{schema, nil}
nil ->
{schema, nil}
end
end
defp expandable?(%Field{fields: fields}), do: !Enum.empty?(fields)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment