Current section
Files
Jump to
Current section
Files
lib/exzeitable/database.ex
defmodule Exzeitable.Database do
@moduledoc "Database interactions"
import Ecto.Query
@doc "Get the data using query"
@spec get_records(map) :: [map]
def get_records(%{query: query} = assigns) do
query
|> order_query(assigns)
|> search_query(assigns)
|> paginate_query(assigns)
|> get_query(assigns)
end
@spec order_query(Ecto.Query.t(), map) :: Ecto.Query.t()
defp order_query(query, %{order: nil}), do: query
defp order_query(query, %{order: order}) do
from(q in exclude(query, :order_by), order_by: ^order)
end
@spec search_query(Ecto.Query.t(), map) :: Ecto.Query.t()
defp search_query(query, %{search: ""}), do: query
defp search_query(query, %{search: search, module: module}) do
apply(module, :do_search, [query, search])
end
@spec remove_order(Ecto.Query.t()) :: Ecto.Query.t()
defp remove_order(query), do: exclude(query, :order_by)
@spec paginate_query(Ecto.Query.t(), map) :: Ecto.Query.t()
defp paginate_query(query, %{per_page: per_page, page: page}) do
offset = if page == 1, do: 0, else: (page - 1) * per_page
from(q in query, limit: ^per_page, offset: ^offset)
end
# Filter out the previous selects and preloads, because we only need the ids to get a count
@spec select_ids(Ecto.Query.t()) :: Ecto.Query.t()
defp select_ids(query) do
query =
query
|> exclude(:select)
|> exclude(:preload)
from(q in query, select: count(q.id))
end
# Repo.all
@spec get_query(Ecto.Query.t(), map) :: [map]
defp get_query(query, %{repo: repo}), do: apply(repo, :all, [query])
@doc "I want to just do a select: count(c.id)"
@spec get_record_count(map) :: integer
def get_record_count(%{query: query} = assigns) do
query
|> select_ids()
|> search_query(assigns)
|> remove_order()
|> get_query(assigns)
|> List.first()
end
@doc "We only want letters to avoid SQL injection attacks"
@spec prefix_search(String.t()) :: String.t()
def prefix_search(terms) do
terms
|> String.trim()
|> String.replace(~r/[^\w\s]|_/u, "")
|> String.replace(~r/\s+/u, ":* & ")
|> Kernel.<>(":*")
end
@doc """
Generates the magic SQL fragment that performs search dynamically.
Created outside macro to bypass ecto restrictions
"""
@spec tsvector_string([keyword]) :: String.t()
def tsvector_string(fields) do
search_columns =
fields
|> Enum.filter(fn {_k, field} -> Keyword.fetch!(field, :search) end)
|> Enum.map(fn {key, _v} -> "coalesce(#{Atom.to_string(key)}, ' ')" end)
|> Enum.join(" || ' ' || ")
"to_tsvector('english', #{search_columns}) @@ to_tsquery(?)"
end
end