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 objects and their relations based on `Ecto.Schema` definitions and offers the ability to translate query resolutions into efficient SQL statements.
## Types
Absinthe objects placed inside your `Types` module:
defmodule MyAppWeb.Schema.Types do
use Absinthe.Schema.Notation
import Dilute
alias MyApp.Blog.{Post, Comment}
dilute_object(Post)
dilute_object(Comment)
end
## Resolution
The resolver can be defined with:
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 ...
defmodule MyAppWeb.Schema do
use Absinthe.Schema
alias MyAppWeb.{Resolver, Schema}
import_types(Schema.Types)
query do
@desc "Get one Post"
field :post, :post do
resolve(&Resolver.resolve/3)
end
end
end
... or the `query_fields/2` macro.
defmodule MyAppWeb.Schema do
use Absinthe.Schema
alias MyAppWeb.{Resolver, Schema}
import_types(Schema.Types)
query do
MyWebApp.Schema.query_fields(:post, &Resolver.resolve/3)
end
end
"""
@doc """
Defines an Absinthe object based on the ecto schema of the given module.
Settings the `:associations` option to `false` will omit the associations in the definition.
Fields can be excluded using the `:exclude` option.
dilute_object Post, exclude: :id do
end
Additionally the do block will override any field definitions.
dilute_object Post do
field(:rating, :float)
end
Ecto allows for Custom Type definitions which have to be overwritten.
"""
@adapters [Dilute.Adapter.Ecto, Dilute.Adapter.Struct]
@default_dilute_object [associations: true, exclude: []]
@default_dilute_input_object [associations: true, exclude: [], prefix: true]
@input_prefix "input_"
alias Dilute.Env
defmacro dilute_object(module) do
quote do
Dilute.dilute_object(unquote(module), [], do: [])
end
end
defmacro dilute_object(module, do: block) do
quote do
Dilute.dilute_object(unquote(module), [], do: unquote(block))
end
end
defmacro dilute_object(module, opts) do
quote do
Dilute.dilute_object(unquote(module), unquote(opts), do: [])
end
end
defmacro dilute_object(module, opts, block) do
with module <- Macro.expand(module, __CALLER__),
{:module, _module} <- Code.ensure_compiled(module) do
quote do
Dilute.__dilute_object__(unquote(module), unquote(opts), unquote(block))
end
else
{:error, error} ->
IO.warn(
"Module could not be compiled, reason: #{error}",
Macro.Env.stacktrace(__CALLER__)
)
end
end
defmacro __dilute_object__(module, opts, do: block) do
opts = Keyword.merge(@default_dilute_object, opts)
env =
module
|> Env.init(__CALLER__)
|> Env.excludes(opts[:exclude])
|> Env.overwrites(block)
|> Env.schema_identifier()
|> Env.adapter(@adapters)
fields =
env
|> Env.fields()
|> resolve_modules()
quote do
object unquote(env.schema) do
unquote([
quote do
Macro.expand_once(unquote(block), unquote(__CALLER__))
end
| for field <- fields do
case field do
{field, :one, type, _related} ->
quote do
field(unquote(field), unquote(type))
end
{field, :many, type, _related} ->
quote do
field(unquote(field), list_of(unquote(type)))
end
end
end
])
end
end
end
defmacro dilute_input_object(module) do
quote do
Dilute.dilute_input_object(unquote(module), [], do: [])
end
end
defmacro dilute_input_object(module, do: block) do
quote do
Dilute.dilute_input_object(unquote(module), [], do: unquote(block))
end
end
defmacro dilute_input_object(module, opts) do
quote do
Dilute.dilute_input_object(unquote(module), unquote(opts), do: [])
end
end
defmacro dilute_input_object(module, opts, block) do
with module <- Macro.expand(module, __CALLER__),
{:module, _module} <- Code.ensure_compiled(module) do
quote do
Dilute.__dilute_input_object__(unquote(module), unquote(opts), unquote(block))
end
else
{:error, error} ->
IO.warn(
"Module could not be compiled, reason: #{error}",
Macro.Env.stacktrace(__CALLER__)
)
end
end
defmacro __dilute_input_object__(module, opts, do: block) do
opts = Keyword.merge(@default_dilute_input_object, opts)
env =
module
|> Env.init(__CALLER__)
|> Env.excludes(opts[:exclude])
|> Env.overwrites(block)
|> Env.schema_identifier(if opts[:prefix], do: @input_prefix, else: "")
|> Env.adapter(@adapters)
fields =
env
|> Env.fields()
|> resolve_modules(@input_prefix)
quote do
input_object unquote(env.schema) do
unquote([
quote do
Macro.expand_once(unquote(block), unquote(__CALLER__))
end
| for field <- fields do
case field do
{field, :one, type, _related} ->
quote do
field(unquote(field), unquote(type))
end
{field, :many, type, _related} ->
quote do
field(unquote(field), list_of(unquote(type)))
end
end
end
])
end
end
end
######
#
# Helper
#
# defp schema_tuple(module, prefix \\ "") when is_atom(module) do
# module
# |> Module.split()
# |> List.last()
# |> Macro.underscore()
# |> (fn schema -> prefix <> schema end).()
# |> (fn schema -> [schema, schema <> "s"] end).()
# |> Enum.map(&String.to_atom/1)
# |> List.to_tuple()
# end
def schema_identifier(module, prefix) do
module
|> Module.split()
|> List.last()
|> Macro.underscore()
|> String.replace_prefix("", prefix)
|> String.to_atom()
end
def resolve_modules(fields, prefix \\ "") do
Enum.map(fields, fn field ->
case field do
{field, cardinality, :"$module", related} ->
{field, cardinality, schema_identifier(related, prefix), related}
field ->
field
end
end)
end
end