diff --git a/swh/graphql/app.py b/swh/graphql/app.py index 5f5a1af..bd6a861 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,18 +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.visit, resolvers.visitstatus, resolvers.snapshot, + resolvers.branch, + resolvers.target, scalars.datetime_scalar, scalars.swhid_scalar, scalars.binary_text_scalar, ) diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py index 00bc55e..c27a32f 100644 --- a/swh/graphql/backends/archive.py +++ b/swh/graphql/backends/archive.py @@ -1,35 +1,34 @@ from swh.storage import get_storage class Archive: def __init__(self): # FIXME, setup config self.storage = get_storage( cls="remote", url="http://moma.internal.softwareheritage.org:5002" ) def get_origin(self, url): return self.storage.origin_get([url])[0] def get_origins(self, after=None, first=50): - # FIXME change page_token to base64 encode return self.storage.origin_list(page_token=after, limit=first) def get_origin_visits(self, origin_url, after=None, first=50): return self.storage.origin_visit_get(origin_url, page_token=after, limit=first) def get_origin_visit(self, origin_url, visit_id): return self.storage.origin_visit_get_by(origin_url, visit_id) def get_visit_status(self, origin_url, visit_id, after=None, first=50): return self.storage.origin_visit_status_get( origin_url, visit_id, page_token=after, limit=first ) def get_snapshot(self, snapshot_swhid): return self.storage.snapshot_get(snapshot_swhid) def get_snapshot_branches(self, snapshot, after=None, first=50): return self.storage.snapshot_get_branches( snapshot, branches_from=after, branches_count=first ) diff --git a/swh/graphql/models/__init__.py b/swh/graphql/models/__init__.py index 19abb55..d2cacf4 100644 --- a/swh/graphql/models/__init__.py +++ b/swh/graphql/models/__init__.py @@ -1,13 +1,17 @@ from .origin import OriginModel +from .release import ReleaseModel +from .revision import RevisionModel from .snapshot import SnapshotModel from .snapshot_branch import SnapshotBranchModel from .visit import VisitModel from .visit_status import VisitStatusModel __all__ = [ "OriginModel", "SnapshotModel", "SnapshotBranchModel", "VisitModel", "VisitStatusModel", + "RevisionModel", + "ReleaseModel", ] diff --git a/swh/graphql/models/base_model.py b/swh/graphql/models/base_model.py index e2a7d0f..4741ac6 100644 --- a/swh/graphql/models/base_model.py +++ b/swh/graphql/models/base_model.py @@ -1,27 +1,30 @@ from abc import ABC from collections import namedtuple class BaseModel(ABC): def __init__(self, node): """ Wrapper class for the model objects SWH storage is not consistent with return types. It is returing object in some cases and dict in some other Mocking an object in case of dict """ # FIXME, this could fail in nested dicts if type(node) is dict: self._node = namedtuple("ModelObj", node.keys())(*node.values()) else: self._node = node 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/models/release.py b/swh/graphql/models/release.py new file mode 100644 index 0000000..ea4b8d6 --- /dev/null +++ b/swh/graphql/models/release.py @@ -0,0 +1,9 @@ +from .base_model import BaseModel + + +class ReleaseModel(BaseModel): + """ + """ + + def is_type_of(self): + return "Release" diff --git a/swh/graphql/models/revision.py b/swh/graphql/models/revision.py new file mode 100644 index 0000000..bbd3a47 --- /dev/null +++ b/swh/graphql/models/revision.py @@ -0,0 +1,9 @@ +from .base_model import BaseModel + + +class RevisionModel(BaseModel): + """ + """ + + def is_type_of(self): + return "Revision" diff --git a/swh/graphql/resolvers/release.py b/swh/graphql/resolvers/release.py new file mode 100644 index 0000000..dad17ac --- /dev/null +++ b/swh/graphql/resolvers/release.py @@ -0,0 +1,11 @@ +from swh.graphql.models import ReleaseModel + +from .base_node import BaseNode + + +class ReleaseNode(BaseNode): + _model_class = ReleaseModel + + def _get_node(self): + # FIXME, make this call async (not for v1) + return {"rel": "test"} diff --git a/swh/graphql/resolvers/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py index b7d2b56..213bae0 100644 --- a/swh/graphql/resolvers/resolver_factory.py +++ b/swh/graphql/resolvers/resolver_factory.py @@ -1,42 +1,46 @@ from .origin import OriginConnection, OriginNode +from .release import ReleaseNode +from .revision import 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, + "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 3e29e7b..fd8ddcb 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,82 +1,102 @@ """ 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 an attribute/item in the object/dict returned by the backend (eg: Origin.url) """ -from ariadne import ObjectType +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 @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 = obj.type + resolver = get_node_resolver(resolver_type) + 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, *_): + """ + To resolve any union type + """ + return obj.is_type_of() diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py new file mode 100644 index 0000000..88d3bc8 --- /dev/null +++ b/swh/graphql/resolvers/revision.py @@ -0,0 +1,11 @@ +from swh.graphql.models import RevisionModel + +from .base_node import BaseNode + + +class RevisionNode(BaseNode): + _model_class = RevisionModel + + def _get_node(self): + # FIXME, make this call async (not for v1) + return {"rev": "test"} diff --git a/swh/graphql/resolvers/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py index a39af4e..92aee99 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,40 +1,48 @@ # from swh.graphql.backends import archive from swh.graphql.models import SnapshotBranchModel # from swh.graphql.utils import utils from swh.storage.interface import PagedResult from .base_connection import BaseConnection class SnapshotBranchConnection(BaseConnection): _model_class = SnapshotBranchModel def _get_page_result(self): + return self._get_from_parent_node() + # FIXME making extra query to the storage - # This is failing now (STORAGEFIX) # This is not really needed as we have the data # in the self.obj itself # Mocking paged data # result = archive.Archive().get_snapshot_branches( - # utils.str_to_swid(self.obj.id.hex()), - # after=self._get_after_arg(), - # first=self._get_first_arg()) + # utils.str_to_swid(self.obj.id.hex()), + # after=self._get_after_arg(), + # first=self._get_first_arg()) # return PagedResult(results=result['branches'], - # next_page_token=result['next_branch']) - return self._get_from_parent_node() + # next_page_token=result['next_branch'].hex()) def _get_from_parent_node(self): """ Branches are avaialble in the snapshot object itself - Not making a query + Not making an extra query """ + results = [ - {"name": key, "type": value["target_type"], "id": "temp-id"} + { + "name": key, + "type": value["target_type"], + "id": "temp-id", + "target": value["target"], + } for (key, value) in self.obj.branches.items() - ] + ][:1] # FIXME, this pagination is broken, fix it with swh-storage + # Mocking PagedResult obj return PagedResult(results=results, next_page_token=self.obj.next_branch) def total_count(self): + # FIXME, this can be implemented with current swh.storage API return None diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index 4d4d210..21fb2c9 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,223 +1,223 @@ 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 an alias type as well +# 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 Revision implements SWHNode { id: SWHId! name: String } # type ReleaseConnection { # } # type ReleasEdge { # } type Release implements SWHNode { id: SWHId! age: String } 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 directory with the given swhid # """ # directory # """ # Get the content with the given swhid # """ # content( # SWHId: String! # ): Content # """ # Search with the given swhid # """ # searchWithSwhid }