Last active
December 1, 2023 14:29
-
-
Save redrabbit/be3528a4e4479886acbe648a693e65c0 to your computer and use it in GitHub Desktop.
Absinthe.Ecto.Resolution.Schema
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 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