zf

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

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