diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py --- a/swh/graphql/backends/archive.py +++ b/swh/graphql/backends/archive.py @@ -59,10 +59,17 @@ ) def get_latest_visit_status( - self, origin_url: str, visit_id: int + self, + origin_url: str, + visit_id: int, + allowed_statuses: Optional[List[str]] = None, + require_snapshot: bool = False, ) -> Optional[OriginVisitStatus]: return self.storage.origin_visit_status_get_latest( - origin_url=origin_url, visit=visit_id + origin_url=origin_url, + visit=visit_id, + allowed_statuses=allowed_statuses, + require_snapshot=require_snapshot, ) def get_origin_snapshots(self, origin_url: str) -> List[Sha1Git]: diff --git a/swh/graphql/errors/__init__.py b/swh/graphql/errors/__init__.py --- a/swh/graphql/errors/__init__.py +++ b/swh/graphql/errors/__init__.py @@ -3,12 +3,18 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from .errors import InvalidInputError, ObjectNotFoundError, PaginationError +from .errors import ( + InvalidInputError, + NullableObjectError, + ObjectNotFoundError, + PaginationError, +) from .handlers import format_error __all__ = [ "ObjectNotFoundError", "PaginationError", "InvalidInputError", + "NullableObjectError", "format_error", ] diff --git a/swh/graphql/errors/errors.py b/swh/graphql/errors/errors.py --- a/swh/graphql/errors/errors.py +++ b/swh/graphql/errors/errors.py @@ -29,3 +29,7 @@ def __init__(self, message, errors=None): super().__init__(f"{self.msg}: {message}") + + +class NullableObjectError(Exception): + pass diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -22,6 +22,7 @@ from graphql.type import GraphQLResolveInfo from swh.graphql import resolvers as rs +from swh.graphql.errors import NullableObjectError from swh.graphql.utils import utils from .resolver_factory import get_connection_resolver, get_node_resolver @@ -76,10 +77,14 @@ @visit.field("latestStatus") def latest_visit_status_resolver( obj: rs.visit.BaseVisitNode, info: GraphQLResolveInfo, **kw -) -> rs.visit_status.LatestVisitStatusNode: +) -> Optional[rs.visit_status.LatestVisitStatusNode]: """ """ resolver = get_node_resolver("latest-status") - return resolver(obj, info, **kw) + try: + return resolver(obj, info, **kw) + except NullableObjectError: + # FIXME, make this pattern generic for all the resolvers + return None @query.field("snapshot") @@ -258,7 +263,7 @@ return resolver(obj, info, **kw) -@visit.field("status") +@visit.field("statuses") def visitstatus_resolver( obj: rs.visit.BaseVisitNode, info: GraphQLResolveInfo, **kw ) -> rs.visit_status.VisitStatusConnection: diff --git a/swh/graphql/resolvers/visit_status.py b/swh/graphql/resolvers/visit_status.py --- a/swh/graphql/resolvers/visit_status.py +++ b/swh/graphql/resolvers/visit_status.py @@ -3,6 +3,7 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +from swh.graphql.errors import NullableObjectError from swh.model.swhids import CoreSWHID, ObjectType from swh.storage.interface import PagedResult @@ -32,7 +33,17 @@ def _get_node_data(self): # self.obj.origin is the origin URL - return self.archive.get_latest_visit_status(self.obj.origin, self.obj.visitId) + return self.archive.get_latest_visit_status( + origin_url=self.obj.origin, + visit_id=self.obj.visitId, + allowed_statuses=self.kwargs.get("allowedStatuses"), + require_snapshot=self.kwargs.get("requireSnapshot"), + ) + + def _handle_node_errors(self) -> None: + # This object can be null + if self._node is None: + raise NullableObjectError("") class VisitStatusConnection(BaseConnection): diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -195,6 +195,18 @@ node: Visit } +""" +Possible visit status states +""" +enum VisitStatusState { + created + ongoing + partial + full + not_found + failed +} + """ An origin visit object """ @@ -222,7 +234,7 @@ """ Connection to all the status objects for the visit """ - status( + statuses( """ Returns the first _n_ elements from the list """ @@ -237,7 +249,17 @@ """ Latest status object for the Visit """ - latestStatus: VisitStatus + latestStatus( + """ + Filter by status state + """ + allowedStatuses: [VisitStatusState] + + """ + Filter by the availability of a snapshot in the status + """ + requireSnapshot: Boolean + ): VisitStatus } """ @@ -287,7 +309,7 @@ """ Status string of the visit (either full, partial or ongoing) """ - status: String! + status: VisitStatusState! """ ISO-8601 encoded date string diff --git a/swh/graphql/tests/functional/test_visit_node.py b/swh/graphql/tests/functional/test_visit_node.py --- a/swh/graphql/tests/functional/test_visit_node.py +++ b/swh/graphql/tests/functional/test_visit_node.py @@ -25,7 +25,7 @@ swhid } } - status { + statuses { nodes { status } @@ -51,7 +51,7 @@ if statuses[-1].snapshot else None, }, - "status": {"nodes": [{"status": status.status} for status in statuses]}, + "statuses": {"nodes": [{"status": status.status} for status in statuses]}, } @@ -64,3 +64,101 @@ } """ assert_missing_object(client, query_str, "visit") + + +def test_get_latest_visit_status_filter_by_status_return_null(client): + query_str = """ + { + visit(originUrl: "%s", visitId: %s) { + visitId + date + type + latestStatus(allowedStatuses: [full]) { + status + } + } + } + """ % ( + get_origins()[0].url, + 1, + ) + data, err = get_query_response(client, query_str) + assert err is None + assert data == { + "visit": { + "date": "2013-05-07T04:20:39.369271+00:00", + "latestStatus": None, + "type": "git", + "visitId": 1, + } + } + + +def test_get_latest_visit_status_filter_by_type(client): + query_str = """ + { + visit(originUrl: "%s", visitId: %s) { + visitId + date + type + latestStatus(allowedStatuses: [ongoing]) { + status + date + } + } + } + """ % ( + get_origins()[0].url, + 1, + ) + data, err = get_query_response(client, query_str) + assert err is None + assert data == { + "visit": { + "date": "2013-05-07T04:20:39.369271+00:00", + "latestStatus": { + "date": "2013-05-07T04:20:39.432222+00:00", + "status": "ongoing", + }, + "type": "git", + "visitId": 1, + } + } + + +def test_get_latest_visit_status_filter_by_snapshot(client): + query_str = """ + { + visit(originUrl: "%s", visitId: %s) { + visitId + date + type + latestStatus(requireSnapshot: true) { + status + date + snapshot { + swhid + } + } + } + } + """ % ( + get_origins()[1].url, + 2, + ) + data, err = get_query_response(client, query_str) + assert err is None + assert data == { + "visit": { + "date": "2015-11-27T17:20:39+00:00", + "latestStatus": { + "date": "2015-11-27T17:22:18+00:00", + "snapshot": { + "swhid": "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" + }, + "status": "partial", + }, + "type": "hg", + "visitId": 2, + } + } diff --git a/swh/graphql/tests/functional/test_visit_status.py b/swh/graphql/tests/functional/test_visit_status.py --- a/swh/graphql/tests/functional/test_visit_status.py +++ b/swh/graphql/tests/functional/test_visit_status.py @@ -16,7 +16,7 @@ query_str = """ { visit(originUrl: "%s", visitId: %s) { - status(first: 3) { + statuses(first: 3) { nodes { status date @@ -33,7 +33,7 @@ visit.visit, ) data, _ = get_query_response(client, query_str) - assert data["visit"]["status"]["nodes"][0] == { + assert data["visit"]["statuses"]["nodes"][0] == { "date": visit_status.date.isoformat(), "snapshot": {"swhid": f"swh:1:snp:{visit_status.snapshot.hex()}"} if visit_status.snapshot is not None