Current section
Files
Jump to
Current section
Files
lib/base.ex
defmodule Filtery.Base do
defmacro __using__(_) do
quote do
import Ecto.Query
import Kernel, except: [apply: 2, apply: 3]
@before_compile Filtery.Base
def apply(query, filters, opts \\ []) do
do_apply(query, filters, opts)
end
defp do_apply(query, filters, opts) when is_map(filters) do
filters = Map.to_list(filters)
do_apply(query, filters, opts)
end
defp do_apply(query, filters, opts) when is_list(filters) do
{sort, filters} = Keyword.pop(filters, :_sort, [])
# skip field starts with underscore
grouped_by_type =
filters
|> Enum.reject(fn
{column, _} -> String.starts_with?(to_string(column), "_")
_ -> false
end)
|> Enum.group_by(fn
{_, {:ref, _}} -> :ref
_ -> :filter
end)
column_filter = grouped_by_type[:filter] || []
query
|> filter_columns(column_filter, opts)
|> join_ref(grouped_by_type[:ref])
|> sort(sort)
end
defp filter_columns(query, filters, opts) do
filters =
if opts[:skip_nil] do
Enum.reject(filters, fn
{_, nil} -> true
{_, {_, nil}} -> true
{_, {_, {_, nil}}} -> true
_ -> false
end)
else
filters
end
# build dynamic conditions
case filter(:and, filters) do
nil -> query
d_query -> where(query, [q], ^d_query)
end
end
# add single column query condition to existing query
def filter(query, column, params) do
case filter(column, params) do
nil -> query
d_query -> where(query, [q], ^d_query)
end
end
@doc """
Apply filter on single column
If filter value is list, filter row that match any value in the list
"""
def filter(:and, filters) when is_map(filters) or is_list(filters) do
Enum.reduce(filters, nil, fn {key, val}, acc ->
ft = filter(key, val)
cond do
nil -> acc
is_nil(acc) -> dynamic([q], ^ft)
ft -> dynamic([q], ^acc and ^ft)
true -> acc
end
end)
end
def filter(:or, filters) when is_map(filters) or is_list(filters) do
Enum.reduce(filters, nil, fn {key, val}, acc ->
ft = filter(key, val)
cond do
nil -> acc
is_nil(acc) -> dynamic([q], ^ft)
ft -> dynamic([q], ^acc or ^ft)
true -> acc
end
end)
end
def filter(:not, filters) when is_map(filters) or is_list(filters) do
d_query = filter(:and, filters)
if d_query do
dynamic([q], not (^d_query))
end
end
end
end
defmacro __before_compile__(_env) do
quote do
def filter(column, {:gt, value}) do
dynamic([q], field(q, ^column) > ^value)
end
def filter(column, {:gte, value}) do
dynamic([q], field(q, ^column) >= ^value)
end
def filter(column, {:lt, value}) do
dynamic([q], field(q, ^column) < ^value)
end
def filter(column, {:lte, value}) do
dynamic([q], field(q, ^column) <= ^value)
end
def filter(column, {:eq, nil}) do
dynamic([q], is_nil(field(q, ^column)))
end
def filter(column, {:eq, :is_nil}) do
dynamic([q], is_nil(field(q, ^column)))
end
def filter(column, {:eq, value}) do
dynamic([q], field(q, ^column) == ^value)
end
def filter(column, {:ne, value}) when value in [nil, :is_nil] do
dynamic([q], not is_nil(field(q, ^column)))
end
def filter(column, {:ne, value}) do
dynamic([q], field(q, ^column) != ^value)
end
def filter(column, {:not, value}) do
ft = filter(column, value)
if ft do
dynamic([q], not (^ft))
end
end
def filter(_column, {:in, nil}) do
nil
end
def filter(column, {:in, values}) do
dynamic([q], field(q, ^column) in ^values)
end
def filter(column, {:nin, values}) do
dynamic([q], field(q, ^column) not in ^values)
end
# extra filters
def filter(column, {:between, [lower, upper]}) do
case [lower, upper] do
[nil, nil] ->
nil
[nil, upper] ->
filter(column, {:lt, upper})
[lower, nil] ->
filter(column, {:gt, lower})
_ ->
filter(:and, [
{column, {:gt, lower}},
{column, {:lt, upper}}
])
end
end
# between inclusive
def filter(column, {:ibetween, [lower, upper]}) do
case [lower, upper] do
[nil, nil] ->
nil
[nil, upper] ->
filter(column, {:lte, upper})
[lower, nil] ->
filter(column, {:gte, lower})
_ ->
filter(:and, [
{column, {:gte, lower}},
{column, {:lte, upper}}
])
end
end
def filter(_, {:between, _}), do: nil
def filter(_, {:ibetween, _}), do: nil
def filter(column, {:has, value}) do
dynamic([q], ^value in field(q, ^column))
end
def filter(column, {:like, value}) do
dynamic([q], like(field(q, ^column), ^"%#{value}%"))
end
def filter(column, {:ilike, value}) do
dynamic([q], ilike(field(q, ^column), ^"%#{value}%"))
end
def filter(column, {:contains, value}) do
filter(column, {:like, value})
end
def filter(column, {:icontains, value}) do
filter(column, {:ilike, value})
end
# DONE: default filter equal
def filter(column, values) when is_list(values) do
filter(column, {:in, values})
end
def filter(column, value) do
filter(column, {:eq, value})
end
defp sort(query, fields) do
if is_nil(fields) or Enum.empty?(fields) do
query
else
order =
fields
|> Enum.map(fn {column, direction} ->
{direction, column}
end)
order_by(query, ^order)
end
end
def join_ref(query, ref, opts \\ [])
def join_ref(query, nil, _), do: query
def join_ref(query, refs, query_opts) when is_list(refs) do
Enum.reduce(refs, query, fn {column, {:ref, ref_filter}}, query ->
join_ref(query, {column, ref_filter}, query_opts)
end)
end
def join_ref(query, {column, {model, filter, opts}}, query_opts) do
foreign_key = Keyword.get(opts, :foreign_key, :"#{column}_id")
references = Keyword.get(opts, :references, :id)
ref_query = __MODULE__.apply(model, filter, query_opts)
join(query, :inner, [a], b in ^ref_query,
on: field(a, ^foreign_key) == field(b, ^references)
)
end
def join_ref(query, _, _), do: query
end
end
end