zf

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

paging.ex (5001B)


      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.GQL.Paging do
     19 @moduledoc "Paging helpers for Resolv modules."
     20 
     21 import Ecto.Query
     22 
     23 alias Zenflows.DB.{ID, Repo, Schema}
     24 
     25 @type result(schema) :: {:ok, t(schema)} | {:error, String.t()}
     26 
     27 @type t(schema) :: %{
     28 	page_info: page_info(),
     29 	edges: [edges(schema)],
     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(schema) :: %{
     42 	cursor: ID.t(),
     43 	node: schema,
     44 }
     45 
     46 @type params() :: %{first: non_neg_integer(), after: ID.t()}
     47 	| %{last: non_neg_integer, before: ID.t()}
     48 
     49 @spec def_page_size() :: non_neg_integer()
     50 def def_page_size() do
     51 	conf()[:def_page_size]
     52 end
     53 
     54 @spec def_page_size() :: non_neg_integer()
     55 def max_page_size() do
     56 	conf()[:max_page_size]
     57 end
     58 
     59 @spec conf() :: Keyword.t()
     60 defp conf() do
     61 	Application.fetch_env!(:zenflows, Zenflows.GQL)
     62 end
     63 
     64 @spec parse(params()) :: {:error, String.t()} | {:ok, {:forw | :back, ID.t() | nil, non_neg_integer()}}
     65 def parse(%{first: _, last: _}),   do: {:error, "first and last can't be provided at the same time"}
     66 def parse(%{after: _, before: _}), do: {:error, "after and before can't be provided at the same time"}
     67 
     68 def parse(%{after: _, last: _}),   do: {:error, "after and last can't be provided at the same time"}
     69 def parse(%{before: _, first: _}), do: {:error, "before and first can't be provided at the same time"}
     70 
     71 def parse(%{first: num}) when num < 0, do: {:error, "first must be positive"}
     72 def parse(%{last: num})  when num < 0, do: {:error, "last must be positive"}
     73 
     74 def parse(%{after: cur, first: num}), do: {:ok, {:forw, cur, normalize(num)}}
     75 def parse(%{after: cur}),             do: {:ok, {:forw, cur, def_page_size()}}
     76 
     77 def parse(%{before: cur, last: num}), do: {:ok, {:back, cur, normalize(num)}}
     78 def parse(%{before: cur}),            do: {:ok, {:back, cur, def_page_size()}}
     79 
     80 def parse(%{first: num}), do: {:ok, {:forw, nil, normalize(num)}}
     81 def parse(%{last: num}),  do: {:ok, {:back, nil, normalize(num)}}
     82 
     83 def parse(_), do: {:ok, {:forw, nil, def_page_size()}}
     84 
     85 @spec normalize(integer()) :: non_neg_integer()
     86 defp normalize(num) do
     87 	# credo:disable-for-next-line Credo.Check.Refactor.MatchInCondition
     88 	if num > (max = max_page_size()),
     89 		do: max,
     90 		else: num
     91 end
     92 
     93 @doc """
     94 Page Ecto schemas.
     95 
     96 Only supports forward or backward paging with or without cursors.
     97 """
     98 @spec page(Schema.t() | Ecto.Query.t(), params()) :: result(Schema.t())
     99 def page(schema_or_query, params) do
    100 	with {:ok, {dir, cur, num}} <- parse(params) do
    101 		{page_fun, order_by} =
    102 			case dir do
    103 				:forw -> {&forw/3, [asc: :id]}
    104 				:back -> {&back/3, [desc: :id]}
    105 				_ -> raise ArgumentError # impossible
    106 			end
    107 		where =
    108 			case {dir, cur} do
    109 				{_, nil} -> []
    110 				{:forw, cur} -> dynamic([s], s.id > ^cur)
    111 				{:back, cur} -> dynamic([s], s.id < ^cur)
    112 				_ -> raise ArgumentError # impossible
    113 			end
    114 		{:ok,
    115 			from(s in schema_or_query,
    116 				where: ^where,
    117 				order_by: ^order_by,
    118 				limit: ^num + 1,
    119 				select: %{cursor: s.id, node: s})
    120 			|> Repo.all()
    121 			|> page_fun.(cur, num)}
    122 	end
    123 end
    124 
    125 @spec forw(edges(Schema.t()), ID.t() | nil, non_neg_integer()) :: t(Schema.t())
    126 def forw(edges, cur, num) do
    127 	{edges, count} =
    128 		Enum.reduce(edges, {[], 0}, fn e, {edges, count} ->
    129 			{[e | edges], count + 1}
    130 		end)
    131 
    132 	{edges, has_next?, count} =
    133 		# we indeed have fetched num+1 records
    134 		if count - 1 == num do
    135 			[_ | edges] = edges
    136 			{edges, true, count - 1}
    137 		else
    138 			{edges, false, count}
    139 		end
    140 
    141 	{edges, first, last} =
    142 		case edges do
    143 			[] -> {[], nil, nil}
    144 			_ ->
    145 				[last | _] = edges
    146 				[first | _] = edges = Enum.reverse(edges)
    147 				{edges, first, last}
    148 		end
    149 
    150 	%{
    151 		edges: edges,
    152 		page_info: %{
    153 			start_cursor: first[:cursor],
    154 			end_cursor: last[:cursor],
    155 			has_next_page: has_next?,
    156 			has_previous_page: cur != nil,
    157 			total_count: count,
    158 			page_limit: num,
    159 		},
    160 	}
    161 end
    162 
    163 @spec back(edges(Schema.t()), ID.t() | nil, non_neg_integer()) :: t(Schema.t())
    164 def back(edges, cur, num) do
    165 	# Currently, this part of the algorithm doesn't care about
    166 	# whether we do forward or backward paging.
    167 	forw(edges, cur, num)
    168 end
    169 end