Current section

Files

Jump to
dilute lib dilute.ex
Raw

lib/dilute.ex

defmodule Dilute do
@moduledoc """
`Ecto.Schema` are very similar to `Absinthe.Type.Object` definitions and are required to be kept in sync.
Dilute is able to derive `Absinthe.Type.Object` on their relations based on `Ecto.Schema` definitions.
## Types
Absinthe objects placed inside your `Types` module:
defmodule MyAppWeb.Schema.Types do
use Absinthe.Schema.Notation
require Dilute
alias MyApp.Blog.{Post, Comment}
Dilute.object(Post)
Dilute.object(Comment)
end
## Resolution
The resolver can be defined as:
defmodule MyAppWeb.Resolver do
use Dilute.Resolver, types: MyAppWeb.Schema.Types, repo: MyApp.Repo
end
Queries can either be defined using the `resolve/3` function or the `query_fields/2` macro
defmodule MyAppWeb.Schema do
use Absinthe.Schema
import_types(MyAppWeb.Schema.Types)
alias BlogWeb.Resolvers
query do
@desc "Get one Post"
field :post, :post do
resolve(&MyAppWeb.Resolver.resolve/3)
end
@desc "Get all Posts"
field :posts, list_of(:post) do
resolve(&MyAppWeb.Resolver.resolve/3)
end
query do
MyWebApp.Schema.query_fields(:post, &Resolver.resolve/3)
end
end
end
"""
import Absinthe.Schema.Notation
require Dilute.Query
@doc """
Defines an Absinthe object based on the ecto schema of the given module.
Fields can be excluded by including the respective field in the `@exclude` attribute:
@exclude [
# ...
{User, [:email, :forename]}
# ...
]
Dilute.object(User)
"""
defmacro object(module) do
module = Macro.expand(module, __CALLER__)
# opts = Keyword.merge(@defaults, opts)
cond do
not Code.ensure_compiled?(module) ->
raise "referenced module #{inspect(module)} could not be complied/loaded"
not function_exported?(module, :__schema__, 2) ->
raise "referenced module #{inspect(module)} is not an Ecto schema"
true ->
:ok
end
schema =
module.__schema__(:source)
|> String.downcase()
|> String.to_atom()
schema_plural =
module.__schema__(:source)
|> String.downcase()
|> (fn schema -> schema <> "s" end).()
|> String.to_atom()
excludes = Module.get_attribute(__CALLER__.module, :excludes) || []
fields = fields(module, Keyword.get(excludes, module, []))
assocs =
module.__schema__(:associations)
|> exclude(excludes)
|> Enum.map(fn field ->
assoc = %{related: mod} = module.__schema__(:association, field)
schema =
mod.__schema__(:source)
|> String.downcase()
|> String.to_atom()
fields = fields(mod, Keyword.get(excludes, mod, []))
{field, assoc, schema, fields}
end)
quote do
def __object__(:schema, unquote(schema)), do: unquote(module)
defmacro query_fields(unquote(schema), resolver) do
fields = unquote(fields)
schema = unquote(schema)
schema_plural = unquote(schema_plural)
quote do
field unquote(schema), unquote(schema) do
unquote(Dilute.args(fields))
resolve(unquote(resolver))
end
field unquote(schema_plural), list_of(unquote(schema)) do
unquote(
for {field, type} <- fields do
quote do
arg(unquote(field), unquote(type))
end
end
)
resolve(unquote(resolver))
end
end
end
object unquote(schema) do
unquote(
for {field, type} <- fields do
quote do
field(unquote(field), unquote(type))
end
end ++
for {field, assoc, schema, fields} <- assocs do
case assoc do
%Ecto.Association.BelongsTo{} ->
quote do
field(unquote(field), unquote(schema)) do
unquote(Dilute.args(fields))
end
end
%Ecto.Association.Has{} ->
quote do
field(unquote(field), list_of(unquote(schema))) do
unquote(Dilute.args(fields))
end
end
_ ->
raise "Dilute is currently only implemented for Ecto's BelongsTo and Has associations"
end
end
)
end
end
end
defp exclude(lst, []) do
lst
end
defp exclude(lst, [h | t]) do
lst
|> List.delete(h)
|> exclude(t)
end
@doc false
def args(fields) do
for {field, type} <- fields do
quote do
arg(unquote(field), unquote(type))
end
end
end
defp fields(module, exclude) do
module.__schema__(:fields)
|> exclude(exclude)
|> Enum.map(fn field ->
{field, module.__schema__(:type, field)}
end)
end
end