connection.ex (4175B)
1 # Zenflows is designed to implement the Valueflows vocabulary, 2 # written and maintained by srfsh <info@dyne.org>. 3 # Copyright (C) 2021-2023 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.Connection do 19 @moduledoc "GraphQL Relay Connection helpers." 20 21 alias Ecto.Changeset 22 alias Zenflows.DB.{ID, Page, Schema, Validate} 23 24 @enforce_keys [:page_info, :edges] 25 defstruct @enforce_keys 26 27 @type t() :: %__MODULE__{ 28 page_info: page_info(), 29 edges: [edge()], 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 edge() :: %{ 42 cursor: ID.t(), 43 node: Ecto.Schema.t(), 44 } 45 46 @doc """ 47 Converts the given list of schemas (that you get by using 48 `Zenflows.DB.Page.all()`) into a Relay connection. 49 """ 50 @spec from_list([Ecto.Schema.t()], Page.t()) :: t() 51 def from_list(records, %{cur: cur, num: num}) do 52 # Currently, we don't differenciate between forwards or 53 # backwards paging, so we only care whether `cur` is nil or 54 # not. 55 56 {edges, count} = 57 Enum.reduce(records, {[], 0}, fn r, {edges, count} -> 58 {[%{cursor: r.id, node: r} | edges], count + 1} 59 end) 60 61 {edges, has_next?, count} = 62 # we indeed have fetched num+1 records 63 if count - 1 == num do 64 [_ | edges] = edges 65 {edges, true, count - 1} 66 else 67 {edges, false, count} 68 end 69 70 {edges, first, last} = 71 case edges do 72 [] -> {[], nil, nil} 73 _ -> 74 [last | _] = edges 75 [first | _] = edges = Enum.reverse(edges) 76 {edges, first, last} 77 end 78 79 %__MODULE__{ 80 edges: edges, 81 page_info: %{ 82 start_cursor: first[:cursor], 83 end_cursor: last[:cursor], 84 has_next_page: has_next?, 85 has_previous_page: cur != nil, 86 total_count: count, 87 page_limit: num, 88 }, 89 } 90 end 91 92 @doc """ 93 Parses a Relay-specific map with filters into a generic 94 `t:Zenflows.DB.Page.t()`. 95 """ 96 @spec parse(Schema.params()) :: {:ok, Page.t()} | {:error, Changeset.t()} 97 def parse(params) do 98 with {:ok, data} <- Changeset.apply_action(changeset(params), nil) do 99 after_ = data[:after] 100 first = data[:first] 101 before = data[:before] 102 last = data[:last] 103 104 {:ok, %Page{ 105 dir: if(before || last, do: :back, else: :forw), 106 cur: after_ || before, 107 num: if((n = first || last), do: normalize(n), else: def_page_size()), 108 filter: data[:filter], 109 }} 110 end 111 end 112 113 @doc false 114 @spec changeset(Schema.params()) :: Changeset.t() 115 def changeset(params) do 116 {%{}, %{after: ID, first: :integer, before: ID, last: :integer, filter: :map}} 117 |> Changeset.cast(params, [:after, :first, :before, :last, :filter]) 118 |> Changeset.validate_number(:first, greater_than_or_equal_to: 0) 119 |> Changeset.validate_number(:last, greater_than_or_equal_to: 0) 120 |> Validate.exist_nand([:first, :last]) 121 |> Validate.exist_nand([:after, :before]) 122 |> Validate.exist_nand([:after, :last]) 123 |> Validate.exist_nand([:before, :first]) 124 end 125 126 @spec normalize(non_neg_integer()) :: non_neg_integer() 127 defp normalize(num) do 128 # credo:disable-for-next-line Credo.Check.Refactor.MatchInCondition 129 if num > (max = max_page_size()), 130 do: max, else: num 131 end 132 133 @doc "The default page size for paging." 134 @spec def_page_size() :: non_neg_integer() 135 def def_page_size(), do: Keyword.fetch!(conf(), :def_page_size) 136 137 @doc "The maximum page size for paging." 138 @spec max_page_size() :: non_neg_integer() 139 def max_page_size(), do: Keyword.fetch!(conf(), :max_page_size) 140 141 @spec conf() :: Keyword.t() 142 defp conf(), do: Application.fetch_env!(:zenflows, Zenflows.GQL) 143 end