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 @@ -3,11 +3,12 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from collections import namedtuple +from typing import Optional, Tuple -from swh.graphql.errors import ObjectNotFoundError +from swh.graphql.errors import DataError from swh.graphql.utils import utils -from swh.storage.interface import PagedResult +from swh.model.model import SnapshotBranch +from swh.storage.interface import PagedResult, PartialBranches from .base_connection import BaseConnection from .base_node import BaseNode @@ -18,17 +19,21 @@ # target field for this node is a UNION type # It is resolved in the top level (resolvers.resolvers.py) - def _get_node_from_data(self, node_data: tuple): - # node_data is a tuple as returned by _get_paged_result in - # SnapshotBranchConnection and _get_node_data in AliasSnapshotBranchNode - # overriding to support this special data structure - branch_name, branch_obj = node_data - node = { - "name": branch_name, - "type": branch_obj.target_type.value, - "target_hash": branch_obj.target, - } - return namedtuple("NodeObj", node.keys())(*node.values()) + def _get_node_from_data( + self, node_data: Optional[Tuple[bytes, Optional[SnapshotBranch]]] + ): + # node_data is received as a tuple from the archive + # (unlike an object or a dict in other cases) + # override this method to make a dict from this special data structure + structured_node_data = None + if node_data is not None: + branch_name, branch_obj = node_data + structured_node_data = { + "name": branch_name, + "type": branch_obj.target_type.value if branch_obj else None, + "target_hash": branch_obj.target if branch_obj else None, + } + return super()._get_node_from_data(structured_node_data) def is_type_of(self): return "Branch" @@ -56,26 +61,28 @@ return parent.swhid parent = parent.obj # Reached the root query node. This will never happen with the current entrypoints - raise ObjectNotFoundError("There is no snapshot associated with the branch") + # raise a DataError in this case + raise DataError( + f"Missing snapshot for the branch {self.target_hash}, last known parent is {parent}" + ) class AliasSnapshotBranchNode(BaseSnapshotBranchNode): + _can_be_null = True obj: BaseSnapshotBranchNode def _get_node_data(self): + target_branch: Optional[bytes] = self.obj.target_hash snapshot_swhid = self.snapshot_swhid() - target_branch = self.obj.target_hash - - alias_branch = self.archive.get_snapshot_branches( + alias_branches: Optional[PartialBranches] = self.archive.get_snapshot_branches( snapshot_swhid.object_id, first=1, name_include=target_branch ) - if target_branch not in alias_branch["branches"]: - raise ObjectNotFoundError( - f"Branch name with {target_branch.decode()} is not available" - ) - # this will be serialized in _get_node_from_data method in the base class - return (target_branch, alias_branch["branches"][target_branch]) + if alias_branches and target_branch in alias_branches["branches"]: + # this will be converted to a dict in _get_node_from_data method in the base class + return (target_branch, alias_branches["branches"][target_branch]) + # FIXME, alias branch is missing, log this event + return None class SnapshotBranchConnection(BaseConnection): @@ -106,7 +113,8 @@ # FIX in swh-storage to return PagedResult # STORAGE-TODO - # this will be serialized in _get_node_from_data method in the node class + # each result item will be converted to a dict in _get_node_from_data method + # in the node class return PagedResult( results=result["branches"].items(), next_page_token=end_cusrsor ) diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py --- a/swh/graphql/tests/data.py +++ b/swh/graphql/tests/data.py @@ -13,6 +13,9 @@ Release, Revision, RevisionType, + Snapshot, + SnapshotBranch, + TargetType, ) from swh.model.tests import swh_model_data @@ -172,7 +175,20 @@ ] +def get_snapshots_with_missing_alias(): + return [ + Snapshot( + branches={ + b"target/missing-alias": SnapshotBranch( + target_type=TargetType.ALIAS, target=b"target/revision" + ), + }, + ), + ] + + GRAPHQL_EXTRA_TEST_OBJECTS = { + "snapshot": get_snapshots_with_missing_alias(), "release": get_releases_with_target(), "revision": get_revisions_with_parents() + get_revisions_with_none_date(), "directory": get_directories_with_nested_path() diff --git a/swh/graphql/tests/functional/test_branch_connection.py b/swh/graphql/tests/functional/test_branch_connection.py --- a/swh/graphql/tests/functional/test_branch_connection.py +++ b/swh/graphql/tests/functional/test_branch_connection.py @@ -6,6 +6,7 @@ import pytest from . import utils +from ..data import get_snapshots_with_missing_alias def get_branches(client, **kwargs) -> tuple: @@ -81,6 +82,20 @@ } +@pytest.mark.parametrize("snapshot", get_snapshots_with_missing_alias()) +def test_get_branches_with_missing_alias(client, snapshot): + data, errors = get_branches( + client, swhid=str(snapshot.swhid()), first=1, types=["alias"] + ) + node = data["snapshot"]["branches"]["nodes"][0] + assert errors is None + assert node == { + "name": {"text": "target/missing-alias"}, + "target": None, + "targetType": "alias", + } + + @pytest.mark.parametrize( "filter_type, count, target_type, swhid_pattern", [