zf

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

commit 2cf3a130c8bb1c6e270cac33e0cdb1072ac2da5b
parent 3ff01c3f0632f00b9fd1cdafa7fa65347461e9cc
Author: srfsh <dev@srf.sh>
Date:   Mon, 22 Aug 2022 18:24:02 +0300

Zenflows.GQL.Paging: init

Diffstat:
Asrc/zenflows/gql/paging.ex | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 169 insertions(+), 0 deletions(-)

diff --git a/src/zenflows/gql/paging.ex b/src/zenflows/gql/paging.ex @@ -0,0 +1,169 @@ +# Zenflows is designed to implement the Valueflows vocabulary, +# written and maintained by srfsh <info@dyne.org>. +# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +defmodule Zenflows.GQL.Paging do +@moduledoc "Paging helpers for Resolv modules." + +import Ecto.Query + +alias Zenflows.DB.{ID, Repo, Schema} + +@type result(schema) :: {:ok, t(schema)} | {:error, String.t()} + +@type t(schema) :: %{ + page_info: page_info(), + edges: [edges(schema)], +} + +@type page_info() :: %{ + start_cursor: ID.t() | nil, + end_cursor: ID.t() | nil, + has_previous_page: boolean(), + has_next_page: boolean(), + total_count: non_neg_integer(), + page_limit: non_neg_integer(), +} + +@type edges(schema) :: %{ + cursor: ID.t(), + node: schema, +} + +@type params() :: %{first: non_neg_integer(), after: ID.t()} + | %{last: non_neg_integer, before: ID.t()} + +@spec def_page_size() :: non_neg_integer() +def def_page_size() do + conf()[:def_page_size] +end + +@spec def_page_size() :: non_neg_integer() +def max_page_size() do + conf()[:max_page_size] +end + +@spec conf() :: Keyword.t() +defp conf() do + Application.fetch_env!(:zenflows, Zenflows.GQL) +end + +@spec parse(params()) :: {:error, String.t()} | {:ok, {:forw | :back, ID.t() | nil, non_neg_integer()}} +def parse(%{first: _, last: _}), do: {:error, "first and last can't be provided at the same time"} +def parse(%{after: _, before: _}), do: {:error, "after and before can't be provided at the same time"} + +def parse(%{after: _, last: _}), do: {:error, "after and last can't be provided at the same time"} +def parse(%{before: _, first: _}), do: {:error, "before and first can't be provided at the same time"} + +def parse(%{first: num}) when num < 0, do: {:error, "first must be positive"} +def parse(%{last: num}) when num < 0, do: {:error, "last must be positive"} + +def parse(%{after: cur, first: num}), do: {:ok, {:forw, cur, normalize(num)}} +def parse(%{after: cur}), do: {:ok, {:forw, cur, def_page_size()}} + +def parse(%{before: cur, last: num}), do: {:ok, {:back, cur, normalize(num)}} +def parse(%{before: cur}), do: {:ok, {:back, cur, def_page_size()}} + +def parse(%{first: num}), do: {:ok, {:forw, nil, normalize(num)}} +def parse(%{last: num}), do: {:ok, {:back, nil, normalize(num)}} + +def parse(_), do: {:ok, {:forw, nil, def_page_size()}} + +@spec normalize(integer()) :: non_neg_integer() +defp normalize(num) do + # credo:disable-for-next-line Credo.Check.Refactor.MatchInCondition + if num > (max = max_page_size()), + do: max, + else: num +end + +@doc """ +Page Ecto schemas. + +Only supports forward or backward paging with or without cursors. +""" +@spec page(Schema.t() | Ecto.Query.t(), params()) :: result(Schema.t()) +def page(schema_or_query, params) do + with {:ok, {dir, cur, num}} <- parse(params) do + {page_fun, order_by} = + case dir do + :forw -> {&forw/3, [asc: :id]} + :back -> {&back/3, [desc: :id]} + _ -> raise ArgumentError # impossible + end + where = + case {dir, cur} do + {_, nil} -> [] + {:forw, cur} -> dynamic([s], s.id > ^cur) + {:back, cur} -> dynamic([s], s.id < ^cur) + _ -> raise ArgumentError # impossible + end + {:ok, + from(s in schema_or_query, + where: ^where, + order_by: ^order_by, + limit: ^num + 1, + select: %{cursor: s.id, node: s}) + |> Repo.all() + |> page_fun.(cur, num)} + end +end + +@spec forw(edges(Schema.t()), ID.t() | nil, non_neg_integer()) :: t(Schema.t()) +def forw(edges, cur, num) do + {edges, count} = + Enum.reduce(edges, {[], 0}, fn e, {edges, count} -> + {[e | edges], count + 1} + end) + + {edges, has_next?, count} = + # we indeed have fetched num+1 records + if count - 1 == num do + [_ | edges] = edges + {edges, true, count - 1} + else + {edges, false, count} + end + + {edges, first, last} = + case edges do + [] -> {[], nil, nil} + _ -> + [last | _] = edges + [first | _] = edges = Enum.reverse(edges) + {edges, first, last} + end + + %{ + edges: edges, + page_info: %{ + start_cursor: first[:cursor], + end_cursor: last[:cursor], + has_next_page: has_next?, + has_previous_page: cur != nil, + total_count: count, + page_limit: num, + }, + } +end + +@spec back(edges(Schema.t()), ID.t() | nil, non_neg_integer()) :: t(Schema.t()) +def back(edges, cur, num) do + # Currently, this part of the algorithm doesn't care about + # whether we do forward or backward paging. + forw(edges, cur, num) +end +end