diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index fa233d9..7be1e8f 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,121 +1,118 @@ from abc import ABC, abstractmethod from typing import Any 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): """ Base class for all the connection resolvers """ _node_class: Any = None _page_size = 50 # default page size - def __init__(self, obj, info, **kwargs): + def __init__(self, obj, info, paged_data=None, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs - - self._paged_data = None - self.pageInfo = self.page_info # To match the name in schema - self.totalCount = self.total_count # To match the name in schema + self._paged_data = paged_data def __call__(self, *args, **kw): return self @property def edges(self): return self._get_edges() @property def nodes(self): """ Override if needed return a list of objects If a node class is set, return a list of its (Node) intances else a list of raw results """ if self._node_class is not None: return [ self._node_class(self.obj, self.info, node_data=result, **self.kwargs) for result in self.get_paged_data().results ] return self.get_paged_data().results @property - def page_info(self): + def pageInfo(self): # To support the schema naming convention # FIXME Replace with a dataclass # return PageInfo(self.page_data.next_page_token) # FIXME, add more details like startCursor return { "hasNextPage": bool(self.get_paged_data().next_page_token), "endCursor": utils.get_encoded_cursor( self.get_paged_data().next_page_token ), } @property - def total_count(self): + def totalCount(self): # To support the schema naming convention """ Will be None for most of the connections override if needed/possible """ return None def get_paged_data(self): """ Cache to avoid multiple calls to the backend (_get_paged_result) return a PagedResult object """ if self._paged_data is None: # FIXME, make this call async (not for v1) self._paged_data = self._get_paged_result() return self._paged_data @abstractmethod def _get_paged_result(self): """ Override for desired behaviour return a PagedResult object """ # FIXME, make this call async (not for v1) 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] 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): """ page_size is set to 50 by default """ return self.kwargs.get("first", self._page_size) diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py index e3e2dc2..af54d0c 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,55 +1,82 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_node import BaseNode class BaseRevisionNode(BaseNode): def _get_revision_by_id(self, revision_id): + # FIXME, make this call async return (archive.Archive().get_revision(revision_id) or None)[0] @property def author(self): # return a PersoneNode object return self._node.author @property def committer(self): # return a PersoneNode object return self._node.committer + @property + def parentIds(self): # To support the schema naming convention + return self._node.parents + + # @paginatedlist + @property + def parents(self): + """ + Return a list of parent revisions + """ + # FIXME, change this to a paginated list + # Storage fix or use paginatedlist decorator + return [ + ParentRevisionNode(obj=self, info=self.info, sha1=revision_id) + for revision_id in self.parentIds + ] + + def is_type_of(self): + """ + is_type_of is required only when + requesting from a connection + + This is for ariadne to return the correct type in schema + """ + return "Revision" + class RevisionNode(BaseRevisionNode): """ When the revision is requested directly (not from a connection) with an id """ def _get_node_data(self): revision_id = utils.str_to_swid(self.kwargs.get("SWHId")) return self._get_revision_by_id(revision_id) +class ParentRevisionNode(BaseRevisionNode): + """ + When a parent revision is requested + """ + + def _get_node_data(self): + revision_id = self.kwargs.get("sha1") + return self._get_revision_by_id(revision_id) + + class BranchRevisionNode(BaseRevisionNode): """ When the revision is requested from a snapshot branch self.obj is a branch object self.obj.target is the revision id """ def _get_node_data(self): """ self.obj.target is the Revision id """ - # FIXME, make this call async (not for v1) return self._get_revision_by_id(self.obj.target) - - def is_type_of(self): - """ - is_type_of is required only when - requesting from a connection - - This is for ariadne to return the correct type in schema - """ - return "Revision" diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index fc78171..f9869ed 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,251 +1,253 @@ scalar SWHId scalar DateTime scalar DateTimeZone scalar BinaryText interface Node { id: ID! } interface SWHNode { id: SWHId! } type PageInfo { endCursor: String hasNextPage: Boolean! } type OriginConnection { edges: [OriginEdge] nodes: [Origin] pageInfo: PageInfo! totalCount: Int } type OriginEdge { cursor: String! node: Origin } type Origin implements SWHNode { id: SWHId! # FIXME, this is not swhid url: String! visits( first: Int after: String ): VisitConnection! } type VisitConnection { edges: [VisitEdge] nodes: [Visit] pageInfo: PageInfo! totalCount: Int } type VisitEdge { cursor: String! node: Visit } type Visit implements Node { id: ID! date: DateTime! type: String status( first: Int after: String ): VisitStatusConnection # origin: Origin # FIXME, this can be added later } type VisitStatusConnection { edges: [VisitStatusEdge] nodes: [VisitStatus] pageInfo: PageInfo! totalCount: Int } type VisitStatusEdge { cursor: String! node: VisitStatus } type VisitStatus implements Node { id: ID! status: String! date: DateTime! snapshot: Snapshot type: String } # FIXME, add OriginSnapshotConnection type Snapshot implements SWHNode { id: SWHId! branches( first: Int after: String ): BranchConnection # releases( # first: Int # after: String # ): ReleaseConnection # FIXME, add alias type as well } type BranchConnection { edges: [BranchConnectionEdge] nodes: [Branch] pageInfo: PageInfo! totalCount: Int } type BranchConnectionEdge { cursor: String! node: [Branch] } # FIXME, this could be alias or Directory as well union BranchTarget = Revision | Release type Branch implements Node { id: ID! name: BinaryText type: String # FIXME, change to an enum target: BranchTarget } # type RevisionConnection { # } # type RevisionEdge { # } type Person { email: BinaryText name: BinaryText fullname: BinaryText } type Revision implements SWHNode { id: SWHId! message: BinaryText author: Person committer: Person date: DateTimeZone type: String directory: SWHId + parentIds: [SWHId] + parents: [Revision] } # type ReleaseConnection { # } # type ReleasEdge { # } type Release implements SWHNode { id: SWHId! name: BinaryText message: BinaryText author: Person date: DateTimeZone } type Directory implements SWHNode { id: SWHId! name: BinaryText entries: BinaryText # FIXME, change to Union type } type Content implements SWHNode { id: SWHId! status: String } 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 also be used to search for an origin """ # FIMXE, use Input types to make this cleaner origins( first: Int after: String ): OriginConnection """ 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 """ Get a snapshot with SWHId """ snapshot( SWHId: String! ): Snapshot # """ # Get all the snapshot for the given origin # """ # originSnapshot( # originUrl: String! # first: Int # after: String # ): SnapshotConnection """ Get the revision with the given swhid """ revision( SWHId: String! ): Revision """ Get the release with the given swhid """ release( SWHId: String! ): Release """ Get the directory with the given swhid """ directory( SWHId: String! ): Directory """ Get the content with the given swhid """ content( SWHId: String! ): Content # """ # Search with the given swhid # """ # searchWithSwhid } diff --git a/swh/graphql/utils/paginate.py b/swh/graphql/utils/paginate.py new file mode 100644 index 0000000..d05d162 --- /dev/null +++ b/swh/graphql/utils/paginate.py @@ -0,0 +1,32 @@ +""" +Pagination at the GraphQL level +This is a temporary fix and inefficient. +Should eventually be moved to the +backend (storage) level +""" + + +class PaginatedList: + def __init__(self, source): + """ + source can be of any iterable type + """ + self.source = source + + def get_items(self, first, after): + """ + Return the 'first' number of + items 'after' the given cursor + """ + return self.source[after : (after + first)] + + def get_item_objects(self, first, after): + """ + Return the 'first' number of + items 'after' the given cursor + with an item cursor + """ + return [ + {"curosr": first + index, "node": item} + for (index, item) in enumerate(self.get_items(first, after), 1) + ]