diff --git a/swh/graphql/app.py b/swh/graphql/app.py --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -30,9 +30,11 @@ resolvers.release, resolvers.directory, resolvers.directory_entry, + resolvers.search_result, resolvers.branch_target, resolvers.release_target, resolvers.directory_entry_target, + resolvers.search_result_target, scalars.id_scalar, scalars.string_scalar, scalars.datetime_scalar, diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -31,8 +31,10 @@ class ContentNode(BaseContentNode): def _get_node_data(self): """ - When a content is requested directly - with its SWHID + When a content is requested directly with its SWHID + + This is also used for resolving SearchTarget when + searched with a content SWHID """ return self._get_content_by_id(self.kwargs.get("swhid").object_id) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -24,6 +24,9 @@ def _get_node_data(self): """ When a directory is requested directly with its SWHID + + This is also used for resolving SearchTarget when + searched with a directory SWHID """ directory_id = self.kwargs.get("swhid").object_id # path = "" diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -13,6 +13,14 @@ def _get_node_data(self): return archive.Archive().get_origin(self.kwargs.get("url")) + def is_type_of(self): + """ + is_type_of is required only when resolving + a UNION type + This is for ariadne to return the right type + """ + return "Origin" + class OriginConnection(BaseConnection): _node_class = OriginNode diff --git a/swh/graphql/resolvers/release.py b/swh/graphql/resolvers/release.py --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -32,6 +32,9 @@ class ReleaseNode(BaseReleaseNode): """ When the release is requested directly with its SWHID + + This is also used for resolving SearchTarget when + searched with a release SWHID """ def _get_node_data(self): diff --git a/swh/graphql/resolvers/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py --- a/swh/graphql/resolvers/resolver_factory.py +++ b/swh/graphql/resolvers/resolver_factory.py @@ -14,6 +14,7 @@ RevisionNode, TargetRevisionNode, ) +from .search import SearchSWHIDNode from .snapshot import OriginSnapshotConnection, SnapshotNode, VisitSnapshotNode from .snapshot_branch import SnapshotBranchConnection from .visit import LatestVisitNode, OriginVisitConnection, OriginVisitNode @@ -42,10 +43,11 @@ "content": ContentNode, "dir-entry-dir": TargetDirectoryNode, "dir-entry-file": TargetContentNode, + "resolve-swhid": SearchSWHIDNode, } - if resolver_type not in mapping: + if resolver_type.lower() not in mapping: raise AttributeError(f"Invalid node type: {resolver_type}") - return mapping[resolver_type] + return mapping[resolver_type.lower()] def get_connection_resolver(resolver_type): @@ -60,6 +62,6 @@ "revision-log": LogRevisionConnection, "directory-entries": DirectoryEntryConnection, } - if resolver_type not in mapping: + if resolver_type.lower() not in mapping: raise AttributeError(f"Invalid connection type: {resolver_type}") - return mapping[resolver_type] + return mapping[resolver_type.lower()] 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 @@ -28,10 +28,12 @@ release: ObjectType = ObjectType("Release") directory: ObjectType = ObjectType("Directory") directory_entry: ObjectType = ObjectType("DirectoryEntry") +search_result: ObjectType = ObjectType("SearchResult") branch_target: UnionType = UnionType("BranchTarget") release_target: UnionType = UnionType("ReleaseTarget") directory_entry_target: UnionType = UnionType("DirectoryEntryTarget") +search_result_target: UnionType = UnionType("SearchResultTarget") # Node resolvers # A node resolver should return an instance of BaseNode @@ -165,6 +167,23 @@ return resolver(obj, info, **kw)() +@query.field("resolveSWHID") +def search_swhid_resolver(obj: None, info: GraphQLResolveInfo, **kw): + resolver = get_node_resolver("resolve-swhid") + return resolver(obj, info, **kw) + + +@search_result.field("target") +def search_swhid_target_resolver( + obj: rs.search.SearchSWHIDNode, info: GraphQLResolveInfo, **kw +): + # Reusing the existing resolvers here (eg: RevisionNode for the rev SWHID) + # TODO; maybe it is better to have a separate class (eg SearchRevisionNode) + resolver = get_node_resolver(obj.targetType) + # Using the kwargs of the parent to reuse the same resolver + return resolver(obj, info, **obj.kwargs) + + # Connection resolvers # A connection resolver should return an instance of BaseConnection @@ -235,6 +254,7 @@ # Any other type of resolver +@search_result_target.type_resolver @release_target.type_resolver @directory_entry_target.type_resolver @branch_target.type_resolver diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -45,6 +45,9 @@ class RevisionNode(BaseRevisionNode): """ When the revision is requested directly with its SWHID + + This is also used for resolving SearchTarget when + searched with a revision SWHID """ def _get_node_data(self): diff --git a/swh/graphql/resolvers/search.py b/swh/graphql/resolvers/search.py new file mode 100644 --- /dev/null +++ b/swh/graphql/resolvers/search.py @@ -0,0 +1,7 @@ +from .base_node import BaseNode + + +class SearchSWHIDNode(BaseNode): + def _get_node_data(self): + swhid = self.kwargs.get("swhid") + return {"targetType": swhid.object_type.name.lower(), "targetSWHID": swhid.object_id} diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -18,10 +18,21 @@ # Same pattern is used in directory return Snapshot(id=snapshot_id, branches={}) + def is_type_of(self): + """ + is_type_of is required only when resolving + a UNION type + This is for ariadne to return the right type + """ + return "Snapshot" + class SnapshotNode(BaseSnapshotNode): """ For directly accessing a snapshot with its SWHID + + This is also used for resolving SearchTarget when + searched with a snapshot SWHID """ def _get_node_data(self): 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 @@ -789,6 +789,78 @@ status: String } +""" +Connection to search results +""" +type SearchResultConnection { + """ + List of search result edges + """ + edges: [SearchResultEdge] + + """ + List of search result nodes + """ + nodes: [SearchResult] + + """ + Information for pagination + """ + pageInfo: PageInfo! + + """ + Total number of search result objects in the connection + """ + totalCount: Int +} + +""" +Edge in search result connection +""" +type SearchResultEdge { + """ + Cursor to request the next page after the item + """ + cursor: String! + + """ + Directory entry object + """ + node: SearchResult +} + +""" +Possible object types in a search result +""" +union SearchResultTarget = Snapshot | Revision | Release | Directory | Content | Origin + +""" +Possible Search target types +""" +enum SearchTargetType { + origin + snapshot + revision + release + directory + content +} + +""" +A search result object +""" +type SearchResult { + """ + Search result object type + """ + targetType: SearchTargetType + + """ + Search result object + """ + target: SearchResultTarget +} + """ The query root of the GraphQL interface. """ @@ -887,4 +959,14 @@ """ swhid: SWHID! ): Content + + """ + Find an object with its SWHID + """ + resolveSWHID( + """ + Returns a single SearchResult object + """ + swhid: SWHID! + ): SearchResult } diff --git a/swh/graphql/tests/unit/resolvers/test_resolver_factory.py b/swh/graphql/tests/unit/resolvers/test_resolver_factory.py --- a/swh/graphql/tests/unit/resolvers/test_resolver_factory.py +++ b/swh/graphql/tests/unit/resolvers/test_resolver_factory.py @@ -31,6 +31,7 @@ ("content", "ContentNode"), ("dir-entry-dir", "TargetDirectoryNode"), ("dir-entry-file", "TargetContentNode"), + ("resolve-swhid", "SearchSWHIDNode"), ], ) def test_get_node_resolver(self, input_type, expected):