mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-01-20 06:37:59 +01:00
146 lines
4.2 KiB
Elixir
146 lines
4.2 KiB
Elixir
defmodule PhilomenaQuery.Cursor do
|
|
alias PhilomenaQuery.Search
|
|
alias Philomena.Repo
|
|
import Ecto.Query
|
|
|
|
@typedoc """
|
|
The underlying cursor type, which contains the ordered sort field values
|
|
of a document.
|
|
"""
|
|
@type cursor :: [integer() | binary() | boolean()]
|
|
|
|
@typedoc """
|
|
A mapping of document IDs to cursors.
|
|
"""
|
|
@type cursor_map :: %{integer() => cursor()}
|
|
|
|
@doc """
|
|
Execute search with optional input cursor, and return results as tuple of
|
|
`{results, cursors}`.
|
|
|
|
## Example
|
|
|
|
iex> search_records(
|
|
...> %{query: ..., sort: [%{created_at: :desc}, %{id: :desc}]}
|
|
...> Image
|
|
...> )
|
|
{%Scrivener.Page{entries: [%Image{id: 1}, ...]},
|
|
%{1 => [1325394000000, 1], ...}}
|
|
|
|
"""
|
|
@spec search_records(Search.search_definition(), Search.queryable(), search_after :: term()) ::
|
|
{Scrivener.Page.t(), cursor_map()}
|
|
def search_records(search_definition, queryable, search_after) do
|
|
search_definition = search_after_definition(search_definition, search_after)
|
|
page = Search.search_records_with_hits(search_definition, queryable)
|
|
|
|
{records, cursors} =
|
|
Enum.map_reduce(page, %{}, fn {record, hit}, cursors ->
|
|
sort = Map.fetch!(hit, "sort")
|
|
|
|
{record, Map.put(cursors, record.id, sort)}
|
|
end)
|
|
|
|
{Map.put(page, :entries, records), cursors}
|
|
end
|
|
|
|
@doc """
|
|
Return page of records and cursors map based on sort.
|
|
|
|
## Example
|
|
|
|
iex> paginate(Forum, [page_size: 25], ["dis", 3], asc: :name, asc: :id)
|
|
%{4 => ["Generals", 4]}
|
|
|
|
"""
|
|
@spec paginate(
|
|
Ecto.Query.t(),
|
|
scrivener_opts :: any(),
|
|
search_after :: term(),
|
|
sorts :: Keyword.t()
|
|
) :: {Scrivener.Page.t(), cursor_map()}
|
|
def paginate(query, pagination, search_after, sorts) do
|
|
total_entries = Repo.aggregate(query, :count)
|
|
pagination = Keyword.merge(pagination, options: [total_entries: total_entries])
|
|
|
|
records =
|
|
query
|
|
|> order_by(^sorts)
|
|
|> search_after_query(search_after, sorts)
|
|
|> Repo.paginate(pagination)
|
|
|
|
fields = Keyword.values(sorts)
|
|
|
|
cursors =
|
|
Enum.reduce(records, %{}, fn record, cursors ->
|
|
field_values = Enum.map(fields, &Map.fetch!(record, &1))
|
|
Map.put(cursors, record.id, field_values)
|
|
end)
|
|
|
|
{records, cursors}
|
|
end
|
|
|
|
@spec search_after_definition(Search.search_definition(), term()) :: Search.search_definition()
|
|
defp search_after_definition(search_definition, search_after) do
|
|
search_after
|
|
|> permit_search_after()
|
|
|> case do
|
|
[] ->
|
|
search_definition
|
|
|
|
search_after ->
|
|
update_in(search_definition.body, &Map.put(&1, :search_after, search_after))
|
|
end
|
|
end
|
|
|
|
@spec search_after_query(Ecto.Query.t(), term(), Keyword.t()) :: Ecto.Query.t()
|
|
defp search_after_query(query, search_after, sorts) do
|
|
search_after = permit_search_after(search_after)
|
|
combined = Enum.zip(sorts, search_after)
|
|
|
|
case combined do
|
|
[_some | _rest] = values ->
|
|
or_clauses = dynamic([], false)
|
|
|
|
{or_clauses, _} =
|
|
Enum.reduce(values, {or_clauses, []}, fn {{sd, col}, value}, {next, equal_parts} ->
|
|
# more specific column has next value
|
|
and_clauses =
|
|
if sd == :asc do
|
|
dynamic([s], field(s, ^col) > ^value)
|
|
else
|
|
dynamic([s], field(s, ^col) < ^value)
|
|
end
|
|
|
|
# and
|
|
and_clauses =
|
|
Enum.reduce(equal_parts, and_clauses, fn {col, value}, rest ->
|
|
# less specific columns are equal
|
|
dynamic([s], field(s, ^col) == ^value and ^rest)
|
|
end)
|
|
|
|
{dynamic(^next or ^and_clauses), equal_parts ++ [{col, value}]}
|
|
end)
|
|
|
|
where(query, ^or_clauses)
|
|
|
|
_ ->
|
|
query
|
|
end
|
|
end
|
|
|
|
# Validate that search_after values are only strings, numbers, and bools
|
|
defp permit_search_after(search_after) do
|
|
search_after
|
|
|> permit_list()
|
|
|> Enum.flat_map(&permit_value/1)
|
|
end
|
|
|
|
defp permit_list(value) when is_list(value), do: value
|
|
defp permit_list(_value), do: []
|
|
|
|
defp permit_value(value) when is_binary(value) or is_number(value) or is_boolean(value),
|
|
do: [value]
|
|
|
|
defp permit_value(_value), do: []
|
|
end
|