defmodule Absinthe.Relay.Connection.Notation do
@moduledoc """
Macros used to define Connection-related schema entities
See `Absinthe.Relay.Connection` for more information.
If you wish to use this module on its own without `use Absinthe.Relay` you
need to include
```
@pipeline_modifier Absinthe.Relay.Schema
```
in your root schema module.
"""
alias Absinthe.Blueprint.Schema
@naming_attrs [:node_type, :non_null, :non_null_edges, :non_null_edge, :non_null_connection, :connection]
defmodule Naming do
@moduledoc false
defstruct base_identifier: nil,
node_type_identifier: nil,
connection_type_identifier: nil,
edge_type_identifier: nil,
non_null_edges: false,
non_null_edge: false,
non_null_connection: false,
attrs: []
def from_attrs!(attrs) do
node_type_identifier =
attrs[:node_type] ||
raise(
"Must provide a `:node_type' option (an optional `:connection` option is also supported)"
)
base_identifier = attrs[:connection] || node_type_identifier
non_null_edges = attrs[:non_null_edges] || attrs[:non_null] || false
non_null_edge = attrs[:non_null_edge] || attrs[:non_null] || false
non_null_connection = attrs[:non_null_connection] || attrs[:non_null] || false
%__MODULE__{
node_type_identifier: node_type_identifier,
base_identifier: base_identifier,
connection_type_identifier: ident(base_identifier, :connection),
edge_type_identifier: ident(base_identifier, :edge),
non_null_edges: non_null_edges,
non_null_edge: non_null_edge,
non_null_connection: non_null_connection,
attrs: [
node_type: node_type_identifier,
connection: base_identifier,
non_null_edges: non_null_edges,
non_null_edge: non_null_edge,
non_null_connection: non_null_connection
]
}
end
defp ident(base, category) do
:"#{base}_#{category}"
end
end
@doc """
Define a connection type for a given node type.
## Examples
A basic connection for a node type, `:pet`. This well generate simple
`:pet_connection` and `:pet_edge` types for you:
```
connection node_type: :pet
```
You can provide a custom name for the connection type (just don't include the
word "connection"). You must still provide the `:node_type`. You can create as
many different connections to a node type as you want.
This example will create a connection type, `:favorite_pets_connection`, and
an edge type, `:favorite_pets_edge`:
```
connection :favorite_pets, node_type: :pet
```
You can customize the connection object just like any other `object`:
```
connection :favorite_pets, node_type: :pet do
field :total_age, :float do
resolve fn
_, %{source: conn} ->
sum = conn.edges
|> Enum.map(fn edge -> edge.node.age)
|> Enum.sum
{:ok, sum}
end
end
edge do
# ...
end
end
```
Just remember that if you use the block form of `connection`, you must call
the `edge` macro within the block to make sure the edge type is generated.
See the `edge` macro below for more information.
"""
defmacro connection({:field, _, [identifier, attrs]}, do: block) when is_list(attrs) do
do_connection_field(identifier, attrs, block)
end
defmacro connection(attrs, do: block) do
naming = Naming.from_attrs!(attrs)
do_connection_definition(naming, attrs, block)
end
defmacro connection(identifier, attrs) do
naming = Naming.from_attrs!(attrs |> Keyword.put(:connection, identifier))
do_connection_definition(naming, attrs, [])
end
defmacro connection(attrs) do
naming = Naming.from_attrs!(attrs)
do_connection_definition(naming, attrs, [])
end
defmacro connection(identifier, attrs, do: block) do
naming = Naming.from_attrs!(attrs |> Keyword.put(:connection, identifier))
do_connection_definition(naming, attrs, block)
end
defp do_connection_field(identifier, attrs, block) do
naming = Naming.from_attrs!(attrs)
paginate = Keyword.get(attrs, :paginate, :both)
field_attrs =
attrs
|> Keyword.drop([:paginate] ++ @naming_attrs)
|> Keyword.put(:type, build_connection_type(naming))
quote do
field unquote(identifier), unquote(field_attrs) do
private(:absinthe_relay, {:paginate, unquote(paginate)}, {:fill, unquote(__MODULE__)})
unquote(block)
end
end
end
defp do_connection_definition(naming, attrs, block) do
identifier = naming.connection_type_identifier
attrs = Keyword.drop(attrs, @naming_attrs)
block = name_edge(block, naming.attrs)
edge_field = build_edge_type(naming)
quote do
object unquote(identifier), unquote(attrs) do
private(
:absinthe_relay,
{:connection, unquote(naming.attrs)},
{:fill, unquote(__MODULE__)}
)
field(:page_info, type: non_null(:page_info))
field(:edges, type: unquote(edge_field))
unquote(block)
end
end
end
defp build_connection_type(%{non_null_connection: true} = naming) do
quote do
non_null(unquote(naming.connection_type_identifier))
end
end
defp build_connection_type(%{non_null_connection: false} = naming) do
naming.connection_type_identifier
end
defp build_edge_type(%{non_null_edge: true, non_null_edges: true} = naming) do
quote do
non_null(list_of(non_null(unquote(naming.edge_type_identifier))))
end
end
defp build_edge_type(%{non_null_edge: true} = naming) do
quote do
list_of(non_null(unquote(naming.edge_type_identifier)))
end
end
defp build_edge_type(%{non_null_edges: true} = naming) do
quote do
non_null(list_of(unquote(naming.edge_type_identifier)))
end
end
defp build_edge_type(naming) do
quote do
list_of(unquote(naming.edge_type_identifier))
end
end
defp name_edge([], _), do: []
defp name_edge({:edge, meta, [[do: block]]}, conn_attrs) do
{:edge, meta, [conn_attrs, [do: block]]}
end
defp name_edge({:__block__, meta, content}, conn_attrs) do
content =
Enum.map(content, fn
{:edge, meta, [[do: block]]} ->
{:edge, meta, [conn_attrs, [do: block]]}
{:edge, meta, [attrs, [do: block]]} ->
{:edge, meta, [conn_attrs ++ attrs, [do: block]]}
node ->
node
end)
{:__block__, meta, content}
end
@doc """
Customize the edge type.
## Examples
```
connection node_type: :pet do
# ...
edge do
field :node_name_backwards, :string do
resolve fn
_, %{source: edge} ->
{:ok, edge.node.name |> String.reverse}
end
end
end
end
```
"""
defmacro edge(attrs, do: block) do
naming = Naming.from_attrs!(attrs)
attrs = Keyword.drop(attrs, @naming_attrs)
quote do
Absinthe.Schema.Notation.stash()
object unquote(naming.edge_type_identifier), unquote(attrs) do
private(:absinthe_relay, {:edge, unquote(naming.attrs)}, {:fill, unquote(__MODULE__)})
unquote(block)
end
Absinthe.Schema.Notation.pop()
end
end
def additional_types({:connection, attrs}, _) do
naming = Naming.from_attrs!(attrs)
identifier = naming.edge_type_identifier
%Schema.ObjectTypeDefinition{
name: identifier |> Atom.to_string() |> Macro.camelize(),
identifier: identifier,
module: __MODULE__,
__private__: [absinthe_relay: [{{:edge, attrs}, {:fill, __MODULE__}}]],
__reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
}
end
def additional_types(_, _), do: []
def fillout({:paginate, type}, node) do
Map.update!(node, :arguments, fn arguments ->
type
|> paginate_args()
|> Enum.map(fn {id, type} -> build_arg(id, type) end)
|> put_uniq(arguments)
end)
end
# @desc "The item at the end of the edge"
# field(:node, unquote(naming.node_type_identifier))
# @desc "A cursor for use in pagination"
# field(:cursor, non_null(:string))
def fillout({:edge, attrs}, node) do
naming = Naming.from_attrs!(attrs)
Map.update!(node, :fields, fn fields ->
naming.node_type_identifier
|> edge_fields
|> put_uniq(fields)
end)
end
def fillout(_, node) do
node
end
defp put_uniq(new, prior) do
existing = MapSet.new(prior, & &1.identifier)
new
|> Enum.filter(&(!(&1.identifier in existing)))
|> Enum.concat(prior)
end
defp edge_fields(node_type) do
[
%Schema.FieldDefinition{
name: "node",
identifier: :node,
type: node_type,
module: __MODULE__,
__reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
},
%Schema.FieldDefinition{
name: "cursor",
identifier: :cursor,
type: :string,
module: __MODULE__,
__reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
}
]
end
defp paginate_args(:forward) do
[after: :string, first: :integer]
end
defp paginate_args(:backward) do
[before: :string, last: :integer]
end
defp paginate_args(:both) do
paginate_args(:forward) ++ paginate_args(:backward)
end
defp build_arg(id, type) do
%Schema.InputValueDefinition{
name: id |> Atom.to_string(),
identifier: id,
type: type,
module: __MODULE__,
__reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
}
end
end