zf

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

commit 3795677744ef6fb9dedd1d024985d10c28bee2d3
parent f8109ba2045a364b90b21882cf83552bb4c71c74
Author: srfsh <dev@srf.sh>
Date:   Thu, 25 Aug 2022 12:31:55 +0300

Zenflows{Test,}.GQL.Paging: rename to Zenflows{Test,}.DB.Paging

Diffstat:
Asrc/zenflows/db/paging.ex | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/zenflows/gql/paging.ex | 169-------------------------------------------------------------------------------
Msrc/zenflows/vf/agent/domain.ex | 4++--
Msrc/zenflows/vf/agent_relationship/domain.ex | 5++---
Msrc/zenflows/vf/agent_relationship_role/domain.ex | 5++---
Msrc/zenflows/vf/agreement/domain.ex | 5++---
Msrc/zenflows/vf/economic_event/domain.ex | 5++---
Msrc/zenflows/vf/economic_resource/domain.ex | 5++---
Msrc/zenflows/vf/intent/domain.ex | 5++---
Msrc/zenflows/vf/organization/domain.ex | 5++---
Msrc/zenflows/vf/person/domain.ex | 5++---
Msrc/zenflows/vf/plan/domain.ex | 5++---
Msrc/zenflows/vf/process/domain.ex | 5++---
Msrc/zenflows/vf/process_specification/domain.ex | 5++---
Msrc/zenflows/vf/product_batch/domain.ex | 5++---
Msrc/zenflows/vf/proposal/domain.ex | 5++---
Msrc/zenflows/vf/proposed_intent/domain.ex | 5++---
Msrc/zenflows/vf/recipe_exchange/domain.ex | 5++---
Msrc/zenflows/vf/recipe_flow/domain.ex | 5++---
Msrc/zenflows/vf/recipe_process/domain.ex | 5++---
Msrc/zenflows/vf/recipe_resource/domain.ex | 5++---
Msrc/zenflows/vf/resource_specification/domain.ex | 5++---
Msrc/zenflows/vf/role_behavior/domain.ex | 5++---
Msrc/zenflows/vf/scenario/domain.ex | 5++---
Msrc/zenflows/vf/scenario_definition/domain.ex | 5++---
Msrc/zenflows/vf/spatial_thing/domain.ex | 5++---
Msrc/zenflows/vf/unit/domain.ex | 6++----
Atest/db/paging.test.exs | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/gql/paging.test.exs | 298-------------------------------------------------------------------------------
29 files changed, 515 insertions(+), 542 deletions(-)

diff --git a/src/zenflows/db/paging.ex b/src/zenflows/db/paging.ex @@ -0,0 +1,167 @@ +# 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.DB.Paging do +@moduledoc "Paging helpers for Domain modules." + +import Ecto.Query + +alias Zenflows.DB.{ID, Repo} + +@type result() :: {:ok, t()} | {:error, String.t()} + +@type t() :: %{ + page_info: page_info(), + edges: [edges()], +} + +@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() :: %{ + cursor: ID.t(), + node: struct(), +} + +@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 max_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(atom() | Ecto.Query.t(), params()) :: result() +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]} + end + where = + case {dir, cur} do + {_, nil} -> [] + {:forw, cur} -> dynamic([s], s.id > ^cur) + {:back, cur} -> dynamic([s], s.id < ^cur) + 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(), ID.t() | nil, non_neg_integer()) :: 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(), ID.t() | nil, non_neg_integer()) :: 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 diff --git a/src/zenflows/gql/paging.ex b/src/zenflows/gql/paging.ex @@ -1,169 +0,0 @@ -# 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 diff --git a/src/zenflows/vf/agent/domain.ex b/src/zenflows/vf/agent/domain.ex @@ -18,7 +18,7 @@ defmodule Zenflows.VF.Agent.Domain do @moduledoc "Domain logic of Agents." -alias Zenflows.DB.Repo +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Agent @typep repo() :: Ecto.Repo.t() @@ -34,7 +34,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Agent.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Agent, params) end diff --git a/src/zenflows/vf/agent_relationship/domain.ex b/src/zenflows/vf/agent_relationship/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.AgentRelationship.Domain do @moduledoc "Domain logic of AgentRelationships." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.AgentRelationship @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(AgentRelationship.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(AgentRelationship, params) end diff --git a/src/zenflows/vf/agent_relationship_role/domain.ex b/src/zenflows/vf/agent_relationship_role/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.AgentRelationshipRole.Domain do @moduledoc "Domain logic of AgentRelationshipRoles." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.AgentRelationshipRole @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(AgentRelationshipRole.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(AgentRelationshipRole, params) end diff --git a/src/zenflows/vf/agreement/domain.ex b/src/zenflows/vf/agreement/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Agreement.Domain do @moduledoc "Domain logic of Agreements." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Agreement @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Agreement.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Agreement, params) end diff --git a/src/zenflows/vf/economic_event/domain.ex b/src/zenflows/vf/economic_event/domain.ex @@ -21,8 +21,7 @@ defmodule Zenflows.VF.EconomicEvent.Domain do import Ecto.Query alias Ecto.{Changeset, Multi} -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.{ Action, EconomicEvent, @@ -46,7 +45,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(EconomicEvent.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(EconomicEvent, params) end diff --git a/src/zenflows/vf/economic_resource/domain.ex b/src/zenflows/vf/economic_resource/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.EconomicResource.Domain do @moduledoc "Domain logic of EconomicResources." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.{ Action, EconomicResource, @@ -43,7 +42,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(EconomicResource.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(EconomicResource, params) end diff --git a/src/zenflows/vf/intent/domain.ex b/src/zenflows/vf/intent/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Intent.Domain do @moduledoc "Domain logic of Intents." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.{ Action, Intent, @@ -43,7 +42,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Intent.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Intent, params) end diff --git a/src/zenflows/vf/organization/domain.ex b/src/zenflows/vf/organization/domain.ex @@ -21,8 +21,7 @@ defmodule Zenflows.VF.Organization.Domain do import Ecto.Query alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Organization @typep repo() :: Ecto.Repo.t() @@ -44,7 +43,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Organization.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(where(Organization, type: :org), params) end diff --git a/src/zenflows/vf/person/domain.ex b/src/zenflows/vf/person/domain.ex @@ -21,8 +21,7 @@ defmodule Zenflows.VF.Person.Domain do import Ecto.Query alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Person @typep repo() :: Ecto.Repo.t() @@ -43,7 +42,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Person.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(where(Person, type: :per), params) end diff --git a/src/zenflows/vf/plan/domain.ex b/src/zenflows/vf/plan/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Plan.Domain do @moduledoc "Domain logic of Plans." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Plan @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Plan.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Plan, params) end diff --git a/src/zenflows/vf/process/domain.ex b/src/zenflows/vf/process/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Process.Domain do @moduledoc "Domain logic of Processes." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Process @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Process.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Process, params) end diff --git a/src/zenflows/vf/process_specification/domain.ex b/src/zenflows/vf/process_specification/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.ProcessSpecification.Domain do @moduledoc "Domain logic of ProcessSpecifications." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.ProcessSpecification @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(ProcessSpecification.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(ProcessSpecification, params) end diff --git a/src/zenflows/vf/product_batch/domain.ex b/src/zenflows/vf/product_batch/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.ProductBatch.Domain do @moduledoc "Domain logic of ProductBatches." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.ProductBatch @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(ProductBatch.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(ProductBatch, params) end diff --git a/src/zenflows/vf/proposal/domain.ex b/src/zenflows/vf/proposal/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Proposal.Domain do @moduledoc "Domain logic of Proposals." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Proposal @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Proposal.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Proposal, params) end diff --git a/src/zenflows/vf/proposed_intent/domain.ex b/src/zenflows/vf/proposed_intent/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.ProposedIntent.Domain do @moduledoc "Domain logic of ProposedIntents." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.ProposedIntent @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(ProposedIntent.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(ProposedIntent, params) end diff --git a/src/zenflows/vf/recipe_exchange/domain.ex b/src/zenflows/vf/recipe_exchange/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.RecipeExchange.Domain do @moduledoc "Domain logic of RecipeExchanges." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.RecipeExchange @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(RecipeExchange.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(RecipeExchange, params) end diff --git a/src/zenflows/vf/recipe_flow/domain.ex b/src/zenflows/vf/recipe_flow/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.RecipeFlow.Domain do @moduledoc "Domain logic of RecipeFlows." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.{ Action, Measure, @@ -43,7 +42,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(RecipeFlow.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(RecipeFlow, params) end diff --git a/src/zenflows/vf/recipe_process/domain.ex b/src/zenflows/vf/recipe_process/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.RecipeProcess.Domain do @moduledoc "Domain logic of RecipeProcesss." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.{Duration, RecipeProcess} @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(RecipeProcess.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(RecipeProcess, params) end diff --git a/src/zenflows/vf/recipe_resource/domain.ex b/src/zenflows/vf/recipe_resource/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.RecipeResource.Domain do @moduledoc "Domain logic of RecipeResources." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.RecipeResource @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(RecipeResource.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(RecipeResource, params) end diff --git a/src/zenflows/vf/resource_specification/domain.ex b/src/zenflows/vf/resource_specification/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.ResourceSpecification.Domain do @moduledoc "Domain logic of ResourceSpecifications." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.ResourceSpecification @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(ResourceSpecification.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(ResourceSpecification, params) end diff --git a/src/zenflows/vf/role_behavior/domain.ex b/src/zenflows/vf/role_behavior/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.RoleBehavior.Domain do @moduledoc "Domain logic of RoleBehaviors." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.RoleBehavior @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(RoleBehavior.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(RoleBehavior, params) end diff --git a/src/zenflows/vf/scenario/domain.ex b/src/zenflows/vf/scenario/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Scenario.Domain do @moduledoc "Domain logic of Scenarios." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Scenario @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Scenario.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Scenario, params) end diff --git a/src/zenflows/vf/scenario_definition/domain.ex b/src/zenflows/vf/scenario_definition/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.ScenarioDefinition.Domain do @moduledoc "Domain logic of ScenarioDefinitions." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.{Duration, ScenarioDefinition} @typep repo() :: Ecto.Repo.t() @@ -39,7 +38,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(ScenarioDefinition.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(ScenarioDefinition, params) end diff --git a/src/zenflows/vf/spatial_thing/domain.ex b/src/zenflows/vf/spatial_thing/domain.ex @@ -20,8 +20,7 @@ defmodule Zenflows.VF.SpatialThing.Domain do # Basically, a fancy name for (geo)location. :P alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.SpatialThing @typep repo() :: Ecto.Repo.t() @@ -40,7 +39,7 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(SpatialThing.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(SpatialThing, params) end diff --git a/src/zenflows/vf/unit/domain.ex b/src/zenflows/vf/unit/domain.ex @@ -19,8 +19,7 @@ defmodule Zenflows.VF.Unit.Domain do @moduledoc "Domain logic of Units." alias Ecto.Multi -alias Zenflows.DB.Repo -alias Zenflows.GQL.Paging +alias Zenflows.DB.{Paging, Repo} alias Zenflows.VF.Unit @typep repo() :: Ecto.Repo.t() @@ -39,12 +38,11 @@ def one(repo, clauses) do end end -@spec all(Paging.params()) :: Paging.result(Unit.t()) +@spec all(Paging.params()) :: Paging.result() def all(params) do Paging.page(Unit, params) end - # `repo` is needed since we use that in a migration script. @spec create(repo(), params()) :: {:ok, Unit.t()} | {:error, chgset()} def create(repo \\ Repo, params) do diff --git a/test/db/paging.test.exs b/test/db/paging.test.exs @@ -0,0 +1,298 @@ +# 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 ZenflowsTest.DB.Paging do +use ZenflowsTest.Help.EctoCase, async: true + +# What we are testing here is a bit interesting. Because, you see, +# what we actually care about is dependant on the number of records we +# ask for (referred to by "num" from now on). This is because of we +# always try to fetch num+1 records. This basically means that we'll +# have a table of possible cases: +# +# num | len(edges) +# ----+----------- +# 0 | 0 +# 0 | 1 +# ----+----------- +# 1 | 0 +# 1 | 1 +# 1 | 2 +# ----+----------- +# 2 | 0 +# 2 | 1 +# 2 | 2 +# 2 | 3 +# ----+----------- +# 3 | 0 +# 3 | 1 +# 3 | 2 +# 3 | 3 +# 3 | 4 +# ----+----------- +# 4 | 0 +# 4 | 1 +# 4 | 2 +# 4 | 3 +# 4 | 4 +# 4 | 5 + +# Here, we cover the cases of: +# num | len(edges) +# ----+----------- +# 0 | 0 +# 1 | 0 +# 2 | 0 +# 3 | 0 +# 4 | 0 +test "num>=0 && len(edges)==0:" do + Enum.each(0..10, fn n -> + assert %{data: %{"people" => data}} = + run!(""" + query ($n: Int!) { + people (first: $n) {...people} + } + """, vars: %{"n" => n}) + + assert [] = Map.fetch!(data, "edges") + + assert %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => 0, + "pageLimit" => ^n, + } = Map.fetch!(data, "pageInfo") + end) +end + +# Here, we cover the cases of: +# num | len(edges) +# ----+----------- +# 1 | 1 +# 2 | 2 +# 3 | 3 +# 4 | 4 +test "num>=1 && len(edges)==num:" do + Enum.reduce(1..10, [], fn n, pers -> + last = %{id: last_cur} = Factory.insert!(:person) + pers = pers ++ [last] + [%{id: first_cur} | _] = pers + + assert %{data: %{"people" => data}} = + run!(""" + query ($n: Int!) { + people (first: $n) {...people} + } + """, vars: %{"n" => n}) + + edges = Map.fetch!(data, "edges") + assert length(edges) == n + + assert %{ + "startCursor" => ^first_cur, + "endCursor" => ^last_cur, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => ^n, + "pageLimit" => ^n, + } = Map.fetch!(data, "pageInfo") + + pers + end) +end + +# Here, we cover the cases of: +# num | len(edges) +# ----+----------- +# 0 | 1 +# 1 | 2 +# 2 | 3 +# 3 | 4 +# 4 | 5 +test "num>=0 && len(edges)==num+1:" do + Enum.reduce(0..10, [], fn n, pers -> + pers = pers ++ [Factory.insert!(:person)] + {tmp, _} = Enum.split(pers, n) + first = List.first(tmp) + last = List.last(tmp) + first_cur = if first != nil, do: first.id, else: nil + last_cur = if last != nil, do: last.id, else: nil + + assert %{data: %{"people" => data}} = + run!(""" + query ($n: Int!) { + people (first: $n) {...people} + } + """, vars: %{"n" => n}) + + edges = Map.fetch!(data, "edges") + assert length(edges) == n + + assert %{ + "startCursor" => ^first_cur, + "endCursor" => ^last_cur, + "hasPreviousPage" => false, + "hasNextPage" => true, + "totalCount" => ^n, + "pageLimit" => ^n, + } = Map.fetch!(data, "pageInfo") + + pers + end) +end + +# Here, we cover the last case, which prooves we cover all the cases +# (this is so because of the fact that we only deal with len(edges)<num +# cases, where num>=1): +# num | len(edges) +# ----+----------- +# 2 | 1 +# ----+----------- +# 3 | 1 +# 3 | 2 +# ----+----------- +# 4 | 1 +# 4 | 2 +# 4 | 3 +test "num>=2 && len(edges)>=0 && len(edges)<num:" do + Enum.reduce(1..9, [], fn e, pers -> + pers = pers ++ [Factory.insert!(:person)] + + Enum.each(2..10, fn n -> + if e < n do + assert %{data: %{"people" => data}} = + run!(""" + query ($n: Int!) { + people (first: $n) {...people} + } + """, vars: %{"n" => n}) + + edges = Map.fetch!(data, "edges") + assert length(edges) == e + + %{id: first_cur} = List.first(pers) + %{id: last_cur} = List.last(pers) + + assert %{ + "startCursor" => ^first_cur, + "endCursor" => ^last_cur, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => ^e, + "pageLimit" => ^n, + } = Map.fetch!(data, "pageInfo") + end + end) + + pers + end) +end + +# We're dealing with cursors here now. Most of the cases are the +# same as the ones without the cursors, so we omit them. + +# Here, we cover the cases of: +# num | len(edges) +# ----+----------- +# 1 | 1 +# 2 | 2 +# 3 | 3 +# 4 | 4 +test "with cursor: num>=1 && len(edges)==num:" do + Enum.each(1..10, fn n -> + p = Factory.insert!(:person) + + assert %{data: %{"people" => data}} = + run!(""" + query ($cur: ID! $n: Int!) { + people (after: $cur first: $n) {...people} + } + """, vars: %{"n" => n, "cur" => p.id}) + + assert [] = Map.fetch!(data, "edges") + + assert %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => true, # spec says so if we can't determine + "hasNextPage" => false, + "totalCount" => 0, + "pageLimit" => ^n, + } = Map.fetch!(data, "pageInfo") + end) +end + +# Here, we cover the cases of: +# num | len(edges) +# ----+----------- +# 1 | 2 +# 2 | 3 +# 3 | 4 +# 4 | 5 +test "with cursor: num>=1 && len(edges)==num+1:" do + pers = [Factory.insert!(:person)] + Enum.reduce(1..10, pers, fn n, pers -> + %{id: after_cur} = List.last(pers) + last = %{id: last_cur} = Factory.insert!(:person) + pers = pers ++ [last] + + assert %{data: %{"people" => data}} = + run!(""" + query ($cur: ID! $n: Int!) { + people (after: $cur first: $n) {...people} + } + """, vars: %{"n" => n, "cur" => after_cur}) + + assert [_] = Map.fetch!(data, "edges") + + assert %{ + "startCursor" => ^last_cur, + "endCursor" => ^last_cur, + "hasPreviousPage" => true, # spec + "hasNextPage" => false, + "totalCount" => 1, + "pageLimit" => ^n, + } = Map.fetch!(data, "pageInfo") + + pers + end) +end + +@spec run!(String.t(), Keyword.t()) :: Absinthe.run_result() +def run!(doc, opts \\ []) do + """ + #{doc} + fragment people on PersonConnection { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {id} + } + } + """ + |> ZenflowsTest.Help.AbsinCase.run!(opts) +end +end diff --git a/test/gql/paging.test.exs b/test/gql/paging.test.exs @@ -1,298 +0,0 @@ -# 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 ZenflowsTest.GQL.Paging do -use ZenflowsTest.Help.EctoCase, async: true - -# What we are testing here is a bit interesting. Because, you see, -# what we actually care about is dependant on the number of records we -# ask for (referred to by "num" from now on). This is because of we -# always try to fetch num+1 records. This basically means that we'll -# have a table of possible cases: -# -# num | len(edges) -# ----+----------- -# 0 | 0 -# 0 | 1 -# ----+----------- -# 1 | 0 -# 1 | 1 -# 1 | 2 -# ----+----------- -# 2 | 0 -# 2 | 1 -# 2 | 2 -# 2 | 3 -# ----+----------- -# 3 | 0 -# 3 | 1 -# 3 | 2 -# 3 | 3 -# 3 | 4 -# ----+----------- -# 4 | 0 -# 4 | 1 -# 4 | 2 -# 4 | 3 -# 4 | 4 -# 4 | 5 - -# Here, we cover the cases of: -# num | len(edges) -# ----+----------- -# 0 | 0 -# 1 | 0 -# 2 | 0 -# 3 | 0 -# 4 | 0 -test "num>=0 && len(edges)==0:" do - Enum.each(0..10, fn n -> - assert %{data: %{"people" => data}} = - run!(""" - query ($n: Int!) { - people (first: $n) {...people} - } - """, vars: %{"n" => n}) - - assert [] = Map.fetch!(data, "edges") - - assert %{ - "startCursor" => nil, - "endCursor" => nil, - "hasPreviousPage" => false, - "hasNextPage" => false, - "totalCount" => 0, - "pageLimit" => ^n, - } = Map.fetch!(data, "pageInfo") - end) -end - -# Here, we cover the cases of: -# num | len(edges) -# ----+----------- -# 1 | 1 -# 2 | 2 -# 3 | 3 -# 4 | 4 -test "num>=1 && len(edges)==num:" do - Enum.reduce(1..10, [], fn n, pers -> - last = %{id: last_cur} = Factory.insert!(:person) - pers = pers ++ [last] - [%{id: first_cur} | _] = pers - - assert %{data: %{"people" => data}} = - run!(""" - query ($n: Int!) { - people (first: $n) {...people} - } - """, vars: %{"n" => n}) - - edges = Map.fetch!(data, "edges") - assert length(edges) == n - - assert %{ - "startCursor" => ^first_cur, - "endCursor" => ^last_cur, - "hasPreviousPage" => false, - "hasNextPage" => false, - "totalCount" => ^n, - "pageLimit" => ^n, - } = Map.fetch!(data, "pageInfo") - - pers - end) -end - -# Here, we cover the cases of: -# num | len(edges) -# ----+----------- -# 0 | 1 -# 1 | 2 -# 2 | 3 -# 3 | 4 -# 4 | 5 -test "num>=0 && len(edges)==num+1:" do - Enum.reduce(0..10, [], fn n, pers -> - pers = pers ++ [Factory.insert!(:person)] - {tmp, _} = Enum.split(pers, n) - first = List.first(tmp) - last = List.last(tmp) - first_cur = if first != nil, do: first.id, else: nil - last_cur = if last != nil, do: last.id, else: nil - - assert %{data: %{"people" => data}} = - run!(""" - query ($n: Int!) { - people (first: $n) {...people} - } - """, vars: %{"n" => n}) - - edges = Map.fetch!(data, "edges") - assert length(edges) == n - - assert %{ - "startCursor" => ^first_cur, - "endCursor" => ^last_cur, - "hasPreviousPage" => false, - "hasNextPage" => true, - "totalCount" => ^n, - "pageLimit" => ^n, - } = Map.fetch!(data, "pageInfo") - - pers - end) -end - -# Here, we cover the last case, which prooves we cover all the cases -# (this is so because of the fact that we only deal with len(edges)<num -# cases, where num>=1): -# num | len(edges) -# ----+----------- -# 2 | 1 -# ----+----------- -# 3 | 1 -# 3 | 2 -# ----+----------- -# 4 | 1 -# 4 | 2 -# 4 | 3 -test "num>=2 && len(edges)>=0 && len(edges)<num:" do - Enum.reduce(1..9, [], fn e, pers -> - pers = pers ++ [Factory.insert!(:person)] - - Enum.each(2..10, fn n -> - if e < n do - assert %{data: %{"people" => data}} = - run!(""" - query ($n: Int!) { - people (first: $n) {...people} - } - """, vars: %{"n" => n}) - - edges = Map.fetch!(data, "edges") - assert length(edges) == e - - %{id: first_cur} = List.first(pers) - %{id: last_cur} = List.last(pers) - - assert %{ - "startCursor" => ^first_cur, - "endCursor" => ^last_cur, - "hasPreviousPage" => false, - "hasNextPage" => false, - "totalCount" => ^e, - "pageLimit" => ^n, - } = Map.fetch!(data, "pageInfo") - end - end) - - pers - end) -end - -# We're dealing with cursors here now. Most of the cases are the -# same as the ones without the cursors, so we omit them. - -# Here, we cover the cases of: -# num | len(edges) -# ----+----------- -# 1 | 1 -# 2 | 2 -# 3 | 3 -# 4 | 4 -test "with cursor: num>=1 && len(edges)==num:" do - Enum.each(1..10, fn n -> - p = Factory.insert!(:person) - - assert %{data: %{"people" => data}} = - run!(""" - query ($cur: ID! $n: Int!) { - people (after: $cur first: $n) {...people} - } - """, vars: %{"n" => n, "cur" => p.id}) - - assert [] = Map.fetch!(data, "edges") - - assert %{ - "startCursor" => nil, - "endCursor" => nil, - "hasPreviousPage" => true, # spec says so if we can't determine - "hasNextPage" => false, - "totalCount" => 0, - "pageLimit" => ^n, - } = Map.fetch!(data, "pageInfo") - end) -end - -# Here, we cover the cases of: -# num | len(edges) -# ----+----------- -# 1 | 2 -# 2 | 3 -# 3 | 4 -# 4 | 5 -test "with cursor: num>=1 && len(edges)==num+1:" do - pers = [Factory.insert!(:person)] - Enum.reduce(1..10, pers, fn n, pers -> - %{id: after_cur} = List.last(pers) - last = %{id: last_cur} = Factory.insert!(:person) - pers = pers ++ [last] - - assert %{data: %{"people" => data}} = - run!(""" - query ($cur: ID! $n: Int!) { - people (after: $cur first: $n) {...people} - } - """, vars: %{"n" => n, "cur" => after_cur}) - - assert [_] = Map.fetch!(data, "edges") - - assert %{ - "startCursor" => ^last_cur, - "endCursor" => ^last_cur, - "hasPreviousPage" => true, # spec - "hasNextPage" => false, - "totalCount" => 1, - "pageLimit" => ^n, - } = Map.fetch!(data, "pageInfo") - - pers - end) -end - -@spec run!(String.t(), Keyword.t()) :: Absinthe.run_result() -def run!(doc, opts \\ []) do - """ - #{doc} - fragment people on PersonConnection { - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - totalCount - pageLimit - } - edges { - cursor - node {id} - } - } - """ - |> ZenflowsTest.Help.AbsinCase.run!(opts) -end -end