Packages
cqrs_tools
0.2.14
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.1
0.1.0
A collection of handy Elixir macros for CQRS applications.
Current section
Files
Jump to
Current section
Files
lib/cqrs/query.ex
defmodule Cqrs.Query do
@moduledoc """
Defines a query and any filters.
## Options
* `require_all_filters` - If `true`, all filters will be required. Defaults to `false`
## Examples
defmodule GetUser do
use Cqrs.Query
alias Cqrs.QueryTest.User
filter :email, :string, required: true
@impl true
def handle_validate(filters, _opts) do
Ecto.Changeset.validate_format(filters, :email, ~r/@/)
end
@impl true
def handle_create([email: email], _opts) do
from u in User, where: u.email == ^email
end
@impl true
def handle_execute(query, opts) do
{:ok, Repo.all(query, opts)}
end
end
### Creation
iex> GetUser.new!()
** (Cqrs.QueryError) %{email: ["can't be blank"]}
iex> GetUser.new!(email: "wrong")
** (Cqrs.QueryError) %{email: ["has invalid format"]}
iex> {:error, %{errors: errors}} = GetUser.new()
...> errors
[email: {"can't be blank", [validation: :required]}]
iex> {:error, %{errors: errors}} = GetUser.new(email: "wrong")
...> errors
[email: {"has invalid format", [validation: :format]}]
iex> {:ok, query} = GetUser.new(email: "chris@example.com")
...> query
#Ecto.Query<from u0 in User, where: u0.email == ^"chris@example.com">
### Execution
iex> {:ok, user} =
...> GetUser.new(email: "chris@example.com")
...> |> GetUser.execute()
...> %{id: user.id, email: user.email}
%{id: "052c1984-74c9-522f-858f-f04f1d4cc786", email: "chris@example.com"}
"""
@type opts :: keyword()
@type query :: Ecto.Query.t()
@type filters :: keyword()
@callback handle_create(filters(), opts()) :: query()
@callback handle_validate(Ecto.Changeset.t(), opts()) :: Ecto.Changeset.t()
@callback handle_execute(Ecto.Query.t(), opts()) ::
{:ok, any()} | {:error, query()} | {:error, any()}
alias Cqrs.{Documentation, Query, QueryError}
defmacro __using__(opts \\ []) do
require_all_filters = Keyword.get(opts, :require_all_filters, false)
quote location: :keep do
Module.register_attribute(__MODULE__, :filters, accumulate: true)
Module.register_attribute(__MODULE__, :required_filters, accumulate: true)
Module.put_attribute(__MODULE__, :require_all_filters, unquote(require_all_filters))
import Ecto.Query
import Query, only: [filter: 2, filter: 3]
@behaviour Query
@before_compile Query
def handle_validate(changeset, _opts), do: changeset
defoverridable handle_validate: 2
end
end
defmacro __before_compile__(_env) do
quote location: :keep do
Query.__introspection__()
Query.__schema__()
Query.__constructor__()
Query.__execute__()
Module.delete_attribute(__MODULE__, :filters)
Module.delete_attribute(__MODULE__, :required_filters)
Module.delete_attribute(__MODULE__, :require_all_filters)
end
end
defmacro __introspection__ do
quote do
require Documentation
@filter_docs Documentation.field_docs("Filters", @filters, @required_filters)
@name __MODULE__ |> Module.split() |> Enum.reverse() |> hd() |> to_string()
def __filters__, do: @filters
def __filter_docs__, do: @filter_docs
def __module_docs__, do: @moduledoc
def __query__, do: __MODULE__
def __name__, do: @name
end
end
defmacro __schema__ do
quote location: :keep do
use Ecto.Schema
@primary_key false
embedded_schema do
Enum.map(@filters, fn
{name, :binary_id, opts} ->
Ecto.Schema.field(name, Ecto.UUID, opts)
{name, type, opts} ->
Ecto.Schema.field(name, type, opts)
end)
end
end
end
defmacro __constructor__ do
quote generated: true, location: :keep do
@spec new(Query.filters(), keyword()) :: {:ok, Ecto.Query.t()} | {:error, any()}
@spec new!(Query.filters(), keyword()) :: Ecto.Query.t()
require Documentation
@doc """
Creates a new `#{__MODULE__} query.`
#{@filter_docs}
"""
def new(filters \\ [], opts \\ []) when is_list(opts),
do: Query.__new__(__MODULE__, filters, @required_filters, opts)
@doc """
Creates a new `#{__MODULE__} query.`
#{@filter_docs}
"""
def new!(filters \\ [], opts \\ []) when is_list(opts),
do: Query.__new__!(__MODULE__, filters, @required_filters, opts)
end
end
defmacro __execute__ do
quote generated: true, location: :keep do
def execute(query, opts \\ []) do
Query.execute(__MODULE__, query, opts)
end
end
end
@doc """
Defines a [Query](`Cqrs.Query`) filter.
* `:name` - any `atom`
* `:type` - any valid [Ecto Schema](`Ecto.Schema`) type
* `:opts` - any valid [Ecto Schema](`Ecto.Schema`) field options. Plus:
* `:required` - `true | false`. Defaults to the `require_all_filters` option.
* `:description` - Documentation for the field.
"""
defmacro filter(name, type, opts \\ []) do
quote location: :keep do
required = Keyword.get(unquote(opts), :required, @require_all_filters)
if required, do: @required_filters(unquote(name))
@filters {unquote(name), unquote(type), Keyword.put(unquote(opts), :required, required)}
end
end
def __new__(mod, filters, required_filters, opts) when is_list(opts) do
fields = mod.__schema__(:fields)
query =
struct(mod)
|> Ecto.Changeset.cast(normalize(filters), fields)
|> Ecto.Changeset.validate_required(required_filters)
|> mod.handle_validate(opts)
|> Ecto.Changeset.apply_action(:create)
case query do
{:ok, query} ->
query =
query
|> Map.from_struct()
|> Enum.reject(&match?({_, nil}, &1))
|> Enum.to_list()
|> mod.handle_create(opts)
{:ok, query}
{:error, query} ->
{:error, query}
end
end
def __new__!(mod, filters, required_filters, opts \\ []) when is_list(opts) do
case __new__(mod, filters, required_filters, opts) do
{:ok, query} -> query
{:error, query} -> raise QueryError, query: query
end
end
defp normalize(values) when is_list(values), do: Enum.into(values, %{})
defp normalize(values) when is_struct(values), do: Map.from_struct(values)
defp normalize(values) when is_map(values), do: values
@doc false
def execute(mod, {:ok, query}, opts), do: mod.handle_execute(query, opts)
def execute(_mod, {:error, query}, _opts), do: {:error, query}
def execute(mod, %Ecto.Query{} = query, opts), do: mod.handle_execute(query, opts)
end