diff --git a/swh/graphql/app.py b/swh/graphql/app.py index bd6a861..f322478 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,20 +1,20 @@ from ariadne import gql, load_schema_from_path, make_executable_schema from .resolvers import resolvers, 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.origins, resolvers.visit, resolvers.visitstatus, resolvers.snapshot, resolvers.branch, resolvers.target, scalars.datetime_scalar, scalars.swhid_scalar, scalars.binary_text_scalar, ) diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index 0cce3df..fa233d9 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,120 +1,121 @@ 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): 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 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): # 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): """ 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/base_node.py b/swh/graphql/resolvers/base_node.py index fe5cc40..f5d6fd0 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,50 +1,51 @@ from abc import ABC, abstractmethod from collections import namedtuple class BaseNode(ABC): """ Base class for all the Node resolvers """ + def __init__(self, obj, info, node_data=None, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._set_node(node_data) def _set_node(self, node_data): if node_data is None: node_data = self._get_node_data() self._node = self._get_node_from_data(node_data) def _get_node_from_data(self, node_data): """ Create an object from the dict Override to support complex data structures """ if type(node_data) is dict: return namedtuple("NodeObj", node_data.keys())(*node_data.values()) return node_data def __call__(self, *args, **kw): return self @abstractmethod def _get_node_data(self): """ Override for desired behaviour This will be called only when node_data is not available """ # FIXME, make this call async (not for v1) return None def __getattr__(self, name): """ Any property defined in the sub-class will get precedence over the _node attributes """ return getattr(self._node, name) def is_type_of(self): return self.__class__.__name__ diff --git a/swh/graphql/resolvers/release.py b/swh/graphql/resolvers/release.py index 5de5dcb..4f88e06 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,35 +1,46 @@ from swh.graphql.backends import archive +from swh.graphql.utils import utils from .base_node import BaseNode -class ReleaseNode(BaseNode): +class BaseReleaseNode(BaseNode): + def _get_release_by_id(self, release_id): + return (archive.Archive().get_release(release_id) or None)[0] + + @property + def author(self): + # return a PersoneNode object + return self._node.author + + +class ReleaseNode(BaseReleaseNode): + """ + When the release is requested directly + (not from a connection) with an id + """ + def _get_node_data(self): - """ - """ + release_id = utils.str_to_swid(self.kwargs.get("SWHId")) + return self._get_release_by_id(release_id) -class BranchReleaseNode(BaseNode): +class BranchReleaseNode(BaseReleaseNode): """ When the release is requested from a snapshot branch self.obj is a branch object self.obj.target is the release id """ def _get_node_data(self): - return (archive.Archive().get_release(self.obj.target) or None)[0] + return self._get_release_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 "Release" - - @property - def author(self): - # return a PersoneNode object - return self.obj.author diff --git a/swh/graphql/resolvers/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py index 2b0e61d..ca0f37a 100644 --- a/swh/graphql/resolvers/resolver_factory.py +++ b/swh/graphql/resolvers/resolver_factory.py @@ -1,46 +1,48 @@ from .origin import OriginConnection, OriginNode -from .release import BranchReleaseNode # , ReleaseNode -from .revision import BranchRevisionNode # , RevisionNode +from .release import BranchReleaseNode, ReleaseNode +from .revision import BranchRevisionNode, RevisionNode from .snapshot import SnapshotNode, VisitSnapshotNode from .snapshot_branch import SnapshotBranchConnection from .visit import OriginVisitConnection, OriginVisitNode from .visit_status import VisitStatusConnection # 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(resolver_type): # FIXME, replace with a proper factory method mapping = { "origin": OriginNode, "visit": OriginVisitNode, "visit-snapshot": VisitSnapshotNode, "snapshot": SnapshotNode, "branch-revision": BranchRevisionNode, "branch-release": BranchReleaseNode, + "revision": RevisionNode, + "release": ReleaseNode, } # resolver_type = get_mapping_key(info) # FIXME, get full name if resolver_type not in mapping: raise AttributeError(f"Invalid type request {resolver_type}") return mapping[resolver_type] def get_connection_resolver(resolver_type): # FIXME, replace with a proper factory method mapping = { "origins": OriginConnection, "origin-visits": OriginVisitConnection, "visit-status": VisitStatusConnection, "snapshot-branches": SnapshotBranchConnection, } # resolver_type = get_mapping_key(info) # FIXME, get full name if resolver_type not in mapping: raise AttributeError(f"Invalid type request {resolver_type}") return mapping[resolver_type] diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index ac3cc7f..c6e7780 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,102 +1,114 @@ """ High level resolvers Any schema attribute can be resolved by any of the following ways and in the following priority order - In this module using an annotation (eg: @visitstatus.field("snapshot")) -- As a property in the model object (eg: models.visit.VisitModel.id) +- As a property in the Node object (eg: resolvers.visit.OriginVisitNode.id) - As an attribute/item in the object/dict returned by the backend (eg: Origin.url) """ from ariadne import ObjectType, UnionType from .resolver_factory import get_connection_resolver, get_node_resolver query = ObjectType("Query") origin = ObjectType("Origin") -origins = ObjectType("OriginConnection") visit = ObjectType("Visit") visitstatus = ObjectType("VisitStatus") snapshot = ObjectType("Snapshot") branch = ObjectType("Branch") + target = UnionType("BranchTarget") # Node resolvers -# A node resolver can return a model object or a data structure +# A node resolver can return a node object or a data structure @query.field("origin") def origin_resolver(obj, info, **kw): """ """ resolver = get_node_resolver("origin") return resolver(obj, info, **kw)() @query.field("visit") def visit_resolver(obj, info, **kw): """ """ resolver = get_node_resolver("visit") return resolver(obj, info, **kw)() @query.field("snapshot") def snapshot_resolver(obj, info, **kw): """ """ resolver = get_node_resolver("snapshot") return resolver(obj, info, **kw)() @visitstatus.field("snapshot") def visit_snapshot(obj, info, **kw): resolver = get_node_resolver("visit-snapshot") return resolver(obj, info, **kw)() @branch.field("target") def branch_target(obj, info, **kw): """ Branch target can be a revision or a release """ resolver_type = f"branch-{obj.type}" resolver = get_node_resolver(resolver_type) return resolver(obj, info, **kw)() +@query.field("revision") +def revision_resolver(obj, info, **kw): + resolver = get_node_resolver("revision") + return resolver(obj, info, **kw)() + + +@query.field("release") +def release_resolver(obj, info, **kw): + resolver = get_node_resolver("release") + return resolver(obj, info, **kw)() + + # Connection resolvers # A connection resolver will return a sub class of BaseConnection @query.field("origins") def origins_resolver(obj, info, **kw): resolver = get_connection_resolver("origins") return resolver(obj, info, **kw)() @origin.field("visits") def visits_resolver(obj, info, **kw): resolver = get_connection_resolver("origin-visits") return resolver(obj, info, **kw)() @visit.field("status") def visitstatus_resolver(obj, info, **kw): resolver = get_connection_resolver("visit-status") return resolver(obj, info, **kw)() @snapshot.field("branches") def snapshot_branches(obj, info, **kw): resolver = get_connection_resolver("snapshot-branches") return resolver(obj, info, **kw)() # Any other type of resolver @target.type_resolver def union_resolver(obj, *_): """ Generic resolver for all the union types """ return obj.is_type_of() diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py index 2185638..e3e2dc2 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,50 +1,55 @@ from swh.graphql.backends import archive +from swh.graphql.utils import utils from .base_node import BaseNode -class RevisionNode(BaseNode): +class BaseRevisionNode(BaseNode): + def _get_revision_by_id(self, revision_id): + 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 + + +class RevisionNode(BaseRevisionNode): """ - When the revision is requested - directly using an id + 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 BranchRevisionNode(BaseNode): +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 (archive.Archive().get_revision(self.obj.target) or None)[0] + 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 """ - # FIXME, this is coupled with the schema return "Revision" - - @property - def author(self): - # return a PersoneNode object - return self.obj.author - - @property - def committer(self): - # return a PersoneNode object - return self.obj.committer diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index d501718..456f801 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,237 +1,246 @@ scalar SWHId scalar DateTime 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 { # FIXME, maybe implement Node is not needed here # As this has no independent existence 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: DateTime type: String directory: SWHId } # type ReleaseConnection { # } # type ReleasEdge { # } type Release implements SWHNode { id: SWHId! name: BinaryText message: BinaryText author: Person date: DateTime } type Directory implements SWHNode { id: SWHId! } type Content implements SWHNode { id: SWHId! } 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() + """ + 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 # """ # Get the content with the given swhid # """ # content( # SWHId: String! # ): Content # """ # Search with the given swhid # """ # searchWithSwhid }