zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

paging.ex (4759B)


      1 # Zenflows is designed to implement the Valueflows vocabulary,
      2 # written and maintained by srfsh <info@dyne.org>.
      3 # Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
      4 #
      5 # This program is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU Affero General Public License as published by
      7 # the Free Software Foundation, either version 3 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU Affero General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU Affero General Public License
     16 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
     17 
     18 defmodule Zenflows.DB.Paging do
     19 @moduledoc "Paging helpers for Domain modules."
     20 
     21 import Ecto.Query
     22 
     23 alias Zenflows.DB.{ID, Repo}
     24 
     25 @type result() :: {:ok, t()} | {:error, String.t()}
     26 
     27 @type t() :: %{
     28 	page_info: page_info(),
     29 	edges: [edges()],
     30 }
     31 
     32 @type page_info() :: %{
     33 	start_cursor: ID.t() | nil,
     34 	end_cursor: ID.t() | nil,
     35 	has_previous_page: boolean(),
     36 	has_next_page: boolean(),
     37 	total_count: non_neg_integer(),
     38 	page_limit: non_neg_integer(),
     39 }
     40 
     41 @type edges() :: %{
     42 	cursor: ID.t(),
     43 	node: struct(),
     44 }
     45 
     46 @type params() :: %{atom() => term()}
     47 
     48 @spec def_page_size() :: non_neg_integer()
     49 def def_page_size() do
     50 	conf()[:def_page_size]
     51 end
     52 
     53 @spec max_page_size() :: non_neg_integer()
     54 def max_page_size() do
     55 	conf()[:max_page_size]
     56 end
     57 
     58 @spec conf() :: Keyword.t()
     59 defp conf() do
     60 	Application.fetch_env!(:zenflows, Zenflows.GQL)
     61 end
     62 
     63 @spec parse(params()) :: {:error, String.t()} | {:ok, {:forw | :back, ID.t() | nil, non_neg_integer()}}
     64 def parse(%{first: _, last: _}),   do: {:error, "first and last can't be provided at the same time"}
     65 def parse(%{after: _, before: _}), do: {:error, "after and before can't be provided at the same time"}
     66 
     67 def parse(%{after: _, last: _}),   do: {:error, "after and last can't be provided at the same time"}
     68 def parse(%{before: _, first: _}), do: {:error, "before and first can't be provided at the same time"}
     69 
     70 def parse(%{first: num}) when num < 0, do: {:error, "first must be positive"}
     71 def parse(%{last: num})  when num < 0, do: {:error, "last must be positive"}
     72 
     73 def parse(%{after: cur, first: num}), do: {:ok, {:forw, cur, normalize(num)}}
     74 def parse(%{after: cur}),             do: {:ok, {:forw, cur, def_page_size()}}
     75 
     76 def parse(%{before: cur, last: num}), do: {:ok, {:back, cur, normalize(num)}}
     77 def parse(%{before: cur}),            do: {:ok, {:back, cur, def_page_size()}}
     78 
     79 def parse(%{first: num}), do: {:ok, {:forw, nil, normalize(num)}}
     80 def parse(%{last: num}),  do: {:ok, {:back, nil, normalize(num)}}
     81 
     82 def parse(_), do: {:ok, {:forw, nil, def_page_size()}}
     83 
     84 @spec normalize(integer()) :: non_neg_integer()
     85 defp normalize(num) do
     86 	# credo:disable-for-next-line Credo.Check.Refactor.MatchInCondition
     87 	if num > (max = max_page_size()),
     88 		do: max,
     89 		else: num
     90 end
     91 
     92 @doc """
     93 Page Ecto schemas.
     94 
     95 Only supports forward or backward paging with or without cursors.
     96 """
     97 @spec page(atom() | Ecto.Query.t(), params()) :: result()
     98 def page(schema_or_query, params) do
     99 	with {:ok, {dir, cur, num}} <- parse(params) do
    100 		{page_fun, order_by} =
    101 			case dir do
    102 				:forw -> {&forw/3, [asc: :id]}
    103 				:back -> {&back/3, [desc: :id]}
    104 			end
    105 		where =
    106 			case {dir, cur} do
    107 				{_, nil} -> []
    108 				{:forw, cur} -> dynamic([s], s.id > ^cur)
    109 				{:back, cur} -> dynamic([s], s.id < ^cur)
    110 			end
    111 		{:ok,
    112 			from(s in schema_or_query,
    113 				where: ^where,
    114 				order_by: ^order_by,
    115 				limit: ^num + 1,
    116 				select: %{cursor: s.id, node: s})
    117 			|> Repo.all()
    118 			|> page_fun.(cur, num)}
    119 	end
    120 end
    121 
    122 @spec forw(edges(), ID.t() | nil, non_neg_integer()) :: t()
    123 def forw(edges, cur, num) do
    124 	{edges, count} =
    125 		Enum.reduce(edges, {[], 0}, fn e, {edges, count} ->
    126 			{[e | edges], count + 1}
    127 		end)
    128 
    129 	{edges, has_next?, count} =
    130 		# we indeed have fetched num+1 records
    131 		if count - 1 == num do
    132 			[_ | edges] = edges
    133 			{edges, true, count - 1}
    134 		else
    135 			{edges, false, count}
    136 		end
    137 
    138 	{edges, first, last} =
    139 		case edges do
    140 			[] -> {[], nil, nil}
    141 			_ ->
    142 				[last | _] = edges
    143 				[first | _] = edges = Enum.reverse(edges)
    144 				{edges, first, last}
    145 		end
    146 
    147 	%{
    148 		edges: edges,
    149 		page_info: %{
    150 			start_cursor: first[:cursor],
    151 			end_cursor: last[:cursor],
    152 			has_next_page: has_next?,
    153 			has_previous_page: cur != nil,
    154 			total_count: count,
    155 			page_limit: num,
    156 		},
    157 	}
    158 end
    159 
    160 @spec back(edges(), ID.t() | nil, non_neg_integer()) :: t()
    161 def back(edges, cur, num) do
    162 	# Currently, this part of the algorithm doesn't care about
    163 	# whether we do forward or backward paging.
    164 	forw(edges, cur, num)
    165 end
    166 end