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