defmodule Versioned.Absinthe do
@moduledoc """
Helpers for Absinthe schemas.
"""
alias Versioned.Helpers
@doc """
Declare an object, versioned compliment, and interface, based off name `name`.
The caller should `use Absinthe.Schema.Notation` as here we return code
which invokes its `object` macro.
Both objects belong to an interface which encompasses the common fields.
All common fields (except `:id` and `:inserted_at`) are included under an
interface, named by the entity name and suffixed `_base`.
The generated object will have the following fields:
* `:id` - ID of the record.
* `:version_id` - ID of the most recent record's version.
* `:inserted_at` - Timestamp when the record was created.
* `:updated_at` - Timestamp when the record was last updated.
* Additionally, all fields declared in the block.
The generated version object will have the following fields:
* `:id` - ID of the version record.
* `:foo_id` - If the entity was `:foo`, then this would be the id of the main
record for which this version is based.
* `:is_deleted` - Boolean indicating if the record was deleted as of this version.
* `:inserted_at` - Timestamp when the version record was created.
Additionally, all fields declared in the block.
## Declaring Fields for Version Object Only
In order for a field to appear only in the version object, use the
`:version_fields` option in the (optional) keyword list before the do block:
versioned_object :car,
version_fields: [person_versions: non_null(list_of(non_null(:person_version)))] do
...
end
"""
defmacro versioned_object(name, opts \\ [], do: block) do
{:__block__, _m, lines_ast} = Helpers.normalize_block(block)
{version_fields, opts} = Keyword.pop(opts, :version_fields)
quote do
object unquote(name), unquote(opts) do
field :id, non_null(:id)
field :version_id, :id
field :inserted_at, non_null(:datetime)
field :updated_at, non_null(:datetime)
unquote(drop_version_fields(block))
interface(unquote(:"#{name}_base"))
end
object unquote(:"#{name}_version") do
field :id, non_null(:id)
field unquote(:"#{name}_id"), :id
field :is_deleted, :boolean
field :inserted_at, :datetime
version_fields(unquote(version_fields))
version_lines(unquote(lines_ast))
interface(unquote(:"#{name}_base"))
end
interface unquote(:"#{name}_base") do
unquote(block)
resolve_type(fn
%{version_id: _}, _ -> unquote(name)
%{unquote(:"#{name}_id") => _}, _ -> unquote(:"#{name}_version")
_, _ -> nil
end)
end
end
end
# Drop `version_field` lines for the base (non-version) object.
@spec drop_version_fields(Macro.t()) :: Macro.t()
defp drop_version_fields({:__block__, top_m, lines}) do
lines = Enum.reject(lines, &match?({:version_field, _, _}, &1))
{:__block__, top_m, lines}
end
defmacro version_fields(fields) do
do_field = fn key, type, opts ->
quote do
field unquote(key), unquote(type), unquote(opts)
end
end
Enum.map(fields || [], fn
{key, {type, opts}} -> do_field.(key, type, opts)
{key, type} -> do_field.(key, type, [])
end)
end
@doc """
Convert a list of ast lines into ast lines to be used for the version object.
"""
defmacro version_lines(lines_ast) do
lines_ast
|> Enum.reduce([], fn
{:version_field, m, a}, acc -> [{:field, m, a} | acc]
other, acc -> [other | acc]
end)
|> Enum.reverse()
end
end