diff --git a/swh/graphql/app.py b/swh/graphql/app.py index 040a006..a6c7a41 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,12 +1,19 @@ from ariadne import load_schema_from_path, make_executable_schema, gql from ariadne.asgi import GraphQL from .resolvers import resolvers +from .resolvers import scalars type_defs = gql(load_schema_from_path("swh/graphql/schema/schema.graphql")) schema = make_executable_schema( - type_defs, resolvers.query, resolvers.origin, resolvers.origins, resolvers.visit + type_defs, + resolvers.query, + resolvers.origin, + resolvers.origins, + resolvers.visit, + scalars.datetime_scalar, + scalars.swhid_scalar, ) app = GraphQL(schema, debug=True) diff --git a/swh/graphql/qns.txt b/swh/graphql/qns.txt new file mode 100644 index 0000000..1e87ec6 --- /dev/null +++ b/swh/graphql/qns.txt @@ -0,0 +1,14 @@ +Questions +========= +* Idea for a homogeneous ID for node (can we expose primary key from postgres) +* Why the visit status is a paginated list in storage?, and not in v1 +* visit id should be visit number +... Schema related questions + +* What should we include in pageinfo (start token, haspreviouspage etc) +* Query cost calculator logic (could be a bit complex) +* Throttling based on Query cost calculator +* Authentication and Authorization + +* Datetime as a time stamp +* Other scalar types needed (swhid) diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index 43f548b..51f160c 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,96 +1,97 @@ """ """ from abc import ABC, abstractmethod from swh.graphql.utils import utils # from dataclasses import dataclass # @dataclass # class PageInfo: # nex_page_token: str # class Arguments: # """ # dataclass # """ # after # Elements that come after the specified cursor # first # Returns the first n elements class BaseConnection(ABC): def __init__(self, obj, info, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._page_data = None self.pageInfo = self.page_info self.totalCount = self.total_count def __call__(self): return self @property def edges(self): return self._get_edges() @property def nodes(self): return self.page_data.results @property def page_info(self): # FIXME Replace with a dataclass # return PageInfo(self.page_data.next_page_token) return { "hasNextPage": bool(self.page_data.next_page_token), "endCursor": utils.get_encoded_cursor(self.page_data.next_page_token), } @property def total_count(self): """ Will be None for most of the connections override if needed """ return None @property def page_data(self): """ Cache to avoid multiple calls to the backend """ if self._page_data is None: # FIXME, make this call async (not for v1) self._page_data = self._get_page_results() return self._page_data @abstractmethod def _get_page_results(self): """ Override for desired behaviour """ - + # FIXME, make this call async (not for v1) return None def _get_edges(self): return [{"cursor": "test", "node": each} for each in self.page_data.results] def _get_after_arg(self): """ Return the decoded next page token + override to use a specific token """ return utils.get_decoded_cursor(self.kwargs.get("after")) def _get_first_arg(self): """ Override to set the default page size """ return self.kwargs.get("first", 50) diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index ec41807..542663c 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,31 +1,31 @@ """ """ from abc import ABC, abstractmethod class BaseNode(ABC): def __init__(self, obj, info, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._node = None def __call__(self): return self.node @property def node(self): # This is a small cache to avoid multiple # backend calls if self._node is None: self._node = self._get_node() return self._node @abstractmethod def _get_node(self): """ Override for desired behaviour """ - + # FIXME, make this call async (not for v1) return None diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index f62fec8..eb44447 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,75 +1,83 @@ from .origin import OriginConnection, OriginNode from .visit import OriginVisitConnection from ariadne import ObjectType query = ObjectType("Query") origin = ObjectType("Origin") origins = ObjectType("OriginConnection") visit = ObjectType("Visit") -def get_node_resolver(resolver_type): +def get_mapping_key(info): + """ + Logic to resolve mapping type + """ + # FIXME, move to utils + if info.path.prev: + return f"{info.path.prev.key}_{info.path.key}" + return info.path.key + + +def get_node_resolver(info): # FIXME, replace with a proper factory method mapping = { "origin": OriginNode, } + resolver_type = get_mapping_key(info) if resolver_type not in mapping: - raise AttributeError("Invalid type request") + raise AttributeError(f"Invalid type request {resolver_type}") return mapping[resolver_type] -def get_connection_resolver(resolver_type): +def get_connection_resolver(info): # FIXME, replace with a proper factory method mapping = {"origins": OriginConnection, "origin_visits": OriginVisitConnection} + resolver_type = get_mapping_key(info) if resolver_type not in mapping: - raise AttributeError("Invalid type request") + raise AttributeError(f"Invalid type request {resolver_type}") return mapping[resolver_type] # Nodes +@query.field("visit") @query.field("origin") def node_resolver(obj, info, **kw): """ Resolver for all the node types """ # FIXME change to static factory in base class - resolver = get_node_resolver(info.field_name) + resolver = get_node_resolver(info) return resolver(obj, info, **kw)() # Resolvers for node fields # Safer to annotate them here than adding as # property in the node class @origin.field("id") def origin_id(origin, info): # Using ariadne decorator to avoid infinite loop issue with id return origin.id.hex() -@visit.field("date") -def visit_date(visit, info): - return visit.date.timestamp() - - @visit.field("id") def visit_id(visit, info): return str(visit.visit) # Connections @origin.field("visits") @query.field("origins") def connection_resolver(obj, info, **kw): # FIXME change to static factory in base class - resolver = get_connection_resolver(info.field_name) + resolver = get_connection_resolver(info) return resolver(obj, info, **kw)() # Other diff --git a/swh/graphql/resolvers/scalars.py b/swh/graphql/resolvers/scalars.py new file mode 100644 index 0000000..876905d --- /dev/null +++ b/swh/graphql/resolvers/scalars.py @@ -0,0 +1,14 @@ +from ariadne import ScalarType + +datetime_scalar = ScalarType("DateTime") +swhid_scalar = ScalarType("SWHId") + + +@datetime_scalar.serializer +def serialize_datetime(value): + return value.timestamp() + + +@swhid_scalar.serializer +def serialize_swid(value): + return value.hex() diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index 62dc9f4..c8112bf 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,10 +1,18 @@ from .base_connection import BaseConnection +from .base_node import BaseNode from swh.graphql.backends import archive +class OriginVisit(BaseNode): + def _get_node(self): + # FIXME, make this call async (not for v1) + pass + + class OriginVisitConnection(BaseConnection): def _get_page_results(self): + # FIXME, make this call async (not for v1) return archive.Archive().get_origin_visits( - self.obj.url, after=self.kwargs.get("after"), first=self.kwargs.get("first") + self.obj.url, after=self._get_after_arg(), first=self._get_first_arg() ) diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index 294f7bb..eca3d28 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,106 +1,118 @@ interface Node { id: ID! } -scalar Date +scalar SWHId + +scalar DateTime type PageInfo { endCursor: String hasNextPage: Boolean! } type Origin implements Node { + # FIXME, find an idea for a UNIQUE id url: String! id: ID! visits( first: Int after: String ): VisitConnection! } type OriginEdge { cursor: String! node: Origin } type OriginConnection { edges: [OriginEdge] nodes: [Origin] pageInfo: PageInfo! totalCount: Int } type Visit implements Node { id: ID! - date: Date! - status: VisitStatusConnection + date: DateTime! + # status: VisitStatusConnection # origin: Origin } type VisitEdge { cursor: String! node: Visit } type VisitConnection { edges: [VisitEdge] nodes: [Visit] pageInfo: PageInfo! totalCount: Int } -type VisitStatus implements Node { - id: ID! - status: String! - date: Date! - snapshot: String - type: String -} +# type VisitStatus implements Node { +# id: ID! +# status: String! +# date: DateTime! +# snapshot: String +# type: String +# } -type VisitStatusEdge { - cursor: String! - node: [VisitStatus] -} +# type VisitStatusEdge { +# cursor: String! +# node: [VisitStatus] +# } -type VisitStatusConnection { - edges: [VisitStatusEdge] - nodes: [VisitStatus] - pageInfo: PageInfo! - totalCount: Int -} +# type VisitStatusConnection { +# edges: [VisitStatusEdge] +# nodes: [VisitStatus] +# pageInfo: PageInfo! +# totalCount: Int +# } -type Snapshot implements Node { - id: ID! - # branches -} +# type Snapshot implements Node { +# id: ID! +# # branches +# } type Query { """ Get an origin with its url """ + # FIXME, find some unique id to help cache + # maybe base64 encode the URL origin( url: String! ): Origin """ Get a list of origins matching the given filters + Can be used to search for an origin """ + # FIMXE, use Input types to make this cleaner origins( first: Int after: String ): OriginConnection! - # visit( - # originUrl: String! - # id: String! - # ): Visit + """ + Get a visit object with its id + and/or origin and visit id + """ + # FIXME, find some unique id to help cache + visit( + originUrl: String! + id: String! + ): Visit } # Remove statusConnection # Make visit obj by making multiple calls # Make snapshot object # Make revisionconnection # Make contnetConnection