diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional, Type +from typing import Any, Optional, Type from swh.graphql.utils import utils @@ -18,6 +18,12 @@ endCursor: str +@dataclass +class ConnectionEdge: + node: Any + cursor: str + + class BaseConnection(ABC): """ Base resolver for all the connections @@ -94,9 +100,13 @@ return None def _get_edges(self): - # FIXME, make cursor work per item - # Cursor can't be None here - return [{"cursor": "dummy", "node": node} for node in self.nodes] + """ + Return the list of connection edges, each with a cursor + """ + return [ + ConnectionEdge(node=node, cursor=self._get_index_cursor(index, node)) + for (index, node) in enumerate(self.nodes) + ] def _get_after_arg(self): """ @@ -110,3 +120,13 @@ page_size is set to 50 by default """ return self.kwargs.get("first", self._page_size) + + def _get_index_cursor(self, index: int, node: Any): + """ + Get the cursor to the given item index + """ + # default implementation which works with swh-storage pagaination + # override this function to support other types (eg: SnapshotBranchConnection) + offset_index = self._get_after_arg() or "0" + index_cursor = int(offset_index) + index + return utils.get_encoded_cursor(str(index_cursor)) diff --git a/swh/graphql/resolvers/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -77,3 +77,7 @@ # the base class, hack to make that work after = utils.get_decoded_cursor(self.kwargs.get("after", "")) return bytes.fromhex(after) + + def _get_index_cursor(self, index: int, node: SnapshotBranchNode): + # Snapshot branch is using a different cursor, hence the override + return utils.get_encoded_cursor(node.name.hex()) diff --git a/swh/graphql/tests/functional/test_pagination.py b/swh/graphql/tests/functional/test_pagination.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/functional/test_pagination.py @@ -0,0 +1,102 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from ..data import get_origins +from .utils import get_query_response + + +# Using Origin object to run functional tests for pagination +def test_pagination(client): + # requesting the max number of nodes available + # endCursor must be None + query_str = f""" + {{ + origins(first: {len(get_origins())}) {{ + nodes {{ + id + }} + pageInfo {{ + hasNextPage + endCursor + }} + }} + }} + """ + + data, _ = get_query_response(client, query_str) + assert len(data["origins"]["nodes"]) == len(get_origins()) + assert data["origins"]["pageInfo"] == {"hasNextPage": False, "endCursor": None} + + +def get_first_node(client): + query_str = """ + { + origins(first: 1) { + nodes { + id + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + data, _ = get_query_response(client, query_str) + return data["origins"] + + +def test_first_arg(client): + origins = get_first_node(client) + assert len(origins["nodes"]) == 1 + assert origins["pageInfo"]["hasNextPage"] is True + + +def test_after_arg(client): + origins = get_first_node(client) + end_cursor = origins["pageInfo"]["endCursor"] + query_str = f""" + {{ + origins(first: 1, after: "{end_cursor}") {{ + nodes {{ + id + }} + pageInfo {{ + hasNextPage + endCursor + }} + }} + }} + """ + data, _ = get_query_response(client, query_str) + assert len(data["origins"]["nodes"]) == 1 + assert data["origins"]["pageInfo"] == {"hasNextPage": False, "endCursor": None} + + +def test_edge_cursor(client): + origins = get_first_node(client) + # end cursor here must be the item cursor for the second item + end_cursor = origins["pageInfo"]["endCursor"] + + query_str = f""" + {{ + origins(first: 1, after: "{end_cursor}") {{ + edges {{ + cursor + node {{ + id + }} + }} + nodes {{ + id + }} + }} + }} + """ + data, _ = get_query_response(client, query_str) + origins = data["origins"] + # nodes in list node fields in edges must be the same + assert [edge["node"] for edge in origins["edges"]] == origins["nodes"] + assert origins["edges"][0]["cursor"] == end_cursor