Packages

Rails like auto generated Ecto functions

Current section

Files

Jump to
active_schema lib active_schema.ex
Raw

lib/active_schema.ex

defmodule Ecto.ActiveSchema do
require Ecto.Query
@date_atoms [:date, :time, :naive_datetime, :utc_datetime]
defmacro __using__(_opts) do
quote do
import Ecto.ActiveSchema
import Ecto.Schema, except: [schema: 2]
@primary_key nil
@timestamps_opts []
@foreign_key_type :id
@schema_prefix nil
@field_source_mapper fn x -> x end
Module.register_attribute(__MODULE__, :ecto_primary_keys, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_fields, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_field_sources, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_assocs, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_embeds, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_raw, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_autogenerate, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_autoupdate, accumulate: true)
Module.put_attribute(__MODULE__, :ecto_autogenerate_id, nil)
end
end
defmacro schema(source, do: block) do
model_ref = __CALLER__.module
field_definitions =
block
|> Tuple.to_list()
|> tl()
|> tl()
|> hd()
|> get_fields()
|> Enum.map(fn field ->
generate(field, model_ref)
end)
quote do
Ecto.Schema.schema unquote(source) do
unquote(block)
end
unquote(
quote do
def unquote(:by_id)(id) do
from(q in unquote(model_ref), where: q.id == ^id)
end
def unquote(:by_id)(query, id) do
from(q in query, where: q.id == ^id)
end
end
)
unquote(
Enum.map(field_definitions, fn field_def ->
quote do
unquote(field_def)
end
end)
)
end
end
defp get_fields(fields) do
fields
|> Enum.map(fn field ->
field_list = Tuple.to_list(field)
type = Enum.at(field_list, 0)
entry = Enum.at(field_list, 2)
if type == :timestamps || is_list(entry) do
Map.merge(%{type: type}, get_data_map(entry))
else
nil
end
end)
|> Enum.reject(&is_nil/1)
end
# Assembles the name and data type fields
defp get_data_map(entry) do
field_name = Enum.at(entry, 0)
data_type = Enum.at(entry, 1)
Map.merge(%{name: field_name}, get_data_type(data_type))
end
# Get the data type for the currently being processed field
defp get_data_type({:__aliases__, _, [:Ecto, :DateTime]}) do
%{data_type: :datetime}
end
defp get_data_type(data) when data in @date_atoms do
%{data_type: :datetime}
end
defp get_data_type({:__aliases__, _, arr}) when is_list(arr) do
%{data_type: Enum.join(arr, ".")}
end
defp get_data_type({:array, _}) do
%{data_type: :array}
end
defp get_data_type([{:through, arr}]) do
%{data_type: :through, through_name: List.last(arr)}
end
defp get_data_type(type) do
%{data_type: type}
end
defp to_name(field_name) when is_nil(field_name) do
nil
end
defp to_name(field_name) when is_atom(field_name) do
Atom.to_string(field_name)
end
# datetime fields
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :field, data_type: :datetime, name: field_name}, structure) do
name = to_name(field_name)
quote do
def unquote(:"by_#{name}")(val) when is_nil(val) do
unquote(structure) |> unquote(:"by_#{name}")(val)
end
def unquote(:"by_#{name}")(query, val) when is_nil(val) do
from(q in query, where: is_nil(q.unquote(field_name)))
end
def unquote(:"by_#{name}")(val) do
unquote(structure) |> unquote(:"by_#{name}")(val)
end
def unquote(:"by_#{name}")(query, val) do
from(q in query, where: q.unquote(field_name) == ^val)
end
def unquote(:"#{name}_after")(date) do
unquote(structure) |> unquote(:"#{name}_after")(date)
end
def unquote(:"#{name}_after")(query, date) do
from(q in query, where: q.unquote(field_name) >= ^date)
end
def unquote(:"#{name}_before")(date) do
unquote(structure) |> unquote(:"#{name}_before")(date)
end
def unquote(:"#{name}_before")(query, date) do
from(q in query, where: q.unquote(field_name) <= ^date)
end
end
end
# boolean fields
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :field, data_type: :boolean, name: field_name}, structure) do
name = to_name(field_name)
quote do
def unquote(:"where_#{name}")(val, params) when is_list(params) do
unquote(structure) |> unquote(:"where_#{name}")(val, params)
end
def unquote(:"where_#{name}")(query, val, params) when is_list(params) do
unquote(:"where_#{name}")(query, val, Enum.into(params, %{}, fn entry -> entry end))
end
def unquote(:"where_#{name}")(query, val, params) when is_map(params) do
query
|> unquote(:"where_#{name}")(val)
|> Ecto.ActiveSchema.order_by(params)
|> Ecto.ActiveSchema.limit(params)
end
def unquote(:"where_#{name}")(val) when is_nil(val) do
unquote(structure) |> unquote(:"where_#{name}")(val)
end
def unquote(:"where_#{name}")(query, val) when is_nil(val) do
from(q in query, where: is_nil(q.unquote(field_name)))
end
def unquote(:"where_#{name}")(val) do
unquote(structure) |> unquote(:"where_#{name}")(val)
end
def unquote(:"where_#{name}")(query, val) do
from(q in query, where: q.unquote(field_name) == ^val)
end
end
end
# array fields
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :field, data_type: :array, name: field_name}, structure) do
name = to_name(field_name)
quote do
# the empty array will be interpreted as query for an empty array
def unquote(:"by_#{name}")(val, params) when params == [] do
from(q in val, where: q.unquote(field_name) == ^params)
end
def unquote(:"by_#{name}")(val, params) when is_list(params) do
# we need to figure out if params is used for ecto seting or a query
pass_through_params =
params
|> Enum.all?(fn element ->
case element do
{k, _v} -> k in [:order_by, :limit]
_ -> false
end
end)
if pass_through_params do
unquote(structure) |> unquote(:"by_#{name}")(val, params)
else
from(q in val, where: q.unquote(field_name) == ^params)
end
end
def unquote(:"by_#{name}")(query, val, params) when is_list(params) do
unquote(:"by_#{name}")(query, val, Enum.into(params, %{}, fn entry -> entry end))
end
def unquote(:"by_#{name}")(query, val, params) when is_map(params) do
query
|> unquote(:"by_#{name}")(val)
|> Ecto.ActiveSchema.order_by(params)
|> Ecto.ActiveSchema.limit(params)
end
def unquote(:"by_#{name}")(val) when is_nil(val) do
unquote(structure) |> unquote(:"by_#{name}")(val)
end
def unquote(:"by_#{name}")(query, val) when is_nil(val) do
from(q in query, where: is_nil(q.unquote(field_name)))
end
def unquote(:"by_#{name}")(val) do
unquote(structure) |> unquote(:"by_#{name}")(val)
end
def unquote(:"by_#{name}")(query, val) do
from(q in query, where: q.unquote(field_name) == ^val)
end
end
end
# all other fields
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :field, data_type: _, name: field_name}, structure) do
name = to_name(field_name)
quote do
def unquote(:"by_#{name}")(val, params) when is_list(params) do
unquote(structure) |> unquote(:"by_#{name}")(val, params)
end
def unquote(:"by_#{name}")(query, val, params) when is_list(params) do
unquote(:"by_#{name}")(query, val, Enum.into(params, %{}, fn entry -> entry end))
end
def unquote(:"by_#{name}")(query, val, params) when is_map(params) do
query
|> unquote(:"by_#{name}")(val)
|> Ecto.ActiveSchema.order_by(params)
|> Ecto.ActiveSchema.limit(params)
end
def unquote(:"by_#{name}")(val) when is_nil(val) do
unquote(structure) |> unquote(:"by_#{name}")(val)
end
def unquote(:"by_#{name}")(query, val) when is_nil(val) do
from(q in query, where: is_nil(q.unquote(field_name)))
end
def unquote(:"by_#{name}")(val) do
unquote(structure) |> unquote(:"by_#{name}")(val)
end
def unquote(:"by_#{name}")(query, val) do
from(q in query, where: q.unquote(field_name) == ^val)
end
end
end
# belongs_to associations
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :belongs_to, data_type: _, name: field_name}, structure) do
name = to_name(field_name)
quote do
def unquote(:"by_#{name}_id")(id) do
unquote(structure) |> unquote(:"by_#{name}_id")(id)
end
def unquote(:"by_#{name}_id")(query, id) do
from(q in query, where: q.unquote(:"#{name}_id") == ^id)
end
def unquote(:"with_#{name}")(query) do
from(
q in query,
left_join: s in assoc(q, unquote(:"#{name}")),
preload: [{unquote(field_name), s}]
)
end
def unquote(:"with_#{name}")() do
from(
q in unquote(structure),
left_join: s in assoc(q, unquote(:"#{name}")),
preload: [{unquote(field_name), s}]
)
end
end
end
# has_one associations
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :has_one, data_type: _, name: field_name}, structure) do
name = to_name(field_name)
quote do
def unquote(:"by_#{name}_id")(id) do
unquote(structure) |> unquote(:"by_#{name}_id")(id)
end
def unquote(:"by_#{name}_id")(query, id) do
from(
q in query,
left_join: s in assoc(q, unquote(:"#{name}")),
where: s.id == ^id
)
end
def unquote(:"with_#{name}")(query) do
from(
q in query,
left_join: s in assoc(q, unquote(:"#{name}")),
preload: [{unquote(field_name), s}]
)
end
def unquote(:"with_#{name}")() do
from(
q in unquote(structure),
left_join: s in assoc(q, unquote(:"#{name}")),
preload: [{unquote(field_name), s}]
)
end
end
end
# has_many associations
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :has_many, data_type: _, name: field_name}, structure) do
name = to_name(field_name)
through_name = Inflex.singularize(name)
quote do
def unquote(:"by_#{through_name}_id")(id) do
unquote(structure) |> unquote(:"by_#{through_name}_id")(id)
end
def unquote(:"by_#{through_name}_id")(query, id) do
from(
q in query,
left_join: s in assoc(q, unquote(:"#{name}")),
where: s.id == ^id
)
end
def unquote(:"with_#{name}")(params) when is_list(params) do
unquote(:"with_#{name}")(from(q in unquote(structure)), params)
end
def unquote(:"with_#{name}")(query, params)
when (is_map(query) or is_atom(query)) and is_list(params) do
unquote(:"with_#{name}")(query, Enum.into(params, %{}, fn entry -> entry end))
end
def unquote(:"with_#{name}")(query, params)
when (is_map(query) or is_atom(query)) and is_map(params) do
query
|> unquote(:"with_#{name}")()
|> Ecto.ActiveSchema.sub_order_by(unquote(field_name), params)
end
def unquote(:"with_#{name}")(query) when is_map(query) do
from(
q in query,
left_join: s in assoc(q, unquote(:"#{name}")),
preload: [{unquote(field_name), s}]
)
end
def unquote(:"with_#{name}")() do
from(
q in unquote(structure),
left_join: s in assoc(q, unquote(:"#{name}")),
preload: [{unquote(field_name), s}]
)
end
end
end
# timestamps
# credo:disable-for-lines:2 Credo.Check.Refactor.ABCSize
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp generate(%{type: :timestamps, data_type: _, name: _}, structure) do
quote do
def inserted_after(date) do
from(q in unquote(structure), where: q.inserted_at >= ^date)
end
def inserted_after(query, date) do
from(q in query, where: q.inserted_at >= ^date)
end
def inserted_before(date) do
from(q in unquote(structure), where: q.inserted_at <= ^date)
end
def inserted_before(query, date) do
from(q in query, where: q.inserted_at <= ^date)
end
def updated_after(date) do
from(q in unquote(structure), where: q.updated_at >= ^date)
end
def updated_after(query, date) do
from(q in query, where: q.updated_at >= ^date)
end
def updated_before(date) do
from(q in unquote(structure), where: q.updated_at <= ^date)
end
def updated_before(query, date) do
from(q in query, where: q.updated_at <= ^date)
end
end
end
def limit(query, %{limit: lim}) when is_number(lim) do
Ecto.Query.limit(query, ^lim)
end
def limit(query, _) do
query
end
def order_by(query, %{order_by: order}) when is_list(order) and length(order) > 0 do
query
|> Ecto.ActiveSchema.order_by(%{order_by: hd(order)})
|> Ecto.ActiveSchema.order_by(%{order_by: tl(order)})
end
def order_by(query, %{order_by: order}) when is_tuple(order) do
Ecto.Query.order_by(query, ^[order])
end
def order_by(query, _) do
query
end
def sub_order_by(query, field_name, %{order_by: ordered_by})
when is_list(ordered_by) and length(ordered_by) > 0 do
query
|> Ecto.ActiveSchema.sub_order_by(field_name, %{order_by: hd(ordered_by)})
|> Ecto.ActiveSchema.sub_order_by(field_name, %{order_by: tl(ordered_by)})
end
def sub_order_by(query, field_name, %{order_by: ordered_by}) when is_tuple(ordered_by) do
# Get the index we need to match against
assoc = query.assocs[field_name]
index = elem(assoc, 0)
order_by_direction = elem(ordered_by, 0)
order_by_field = elem(ordered_by, 1)
Map.put(
query,
:order_bys,
query.order_bys ++
[
%Ecto.Query.QueryExpr{
expr: [
{order_by_direction, {{:., [], [{:&, [], [index]}, order_by_field]}, [], []}}
]
}
]
)
end
def sub_order_by(query, _, _) do
query
end
end