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, resolvers.binary_string, scalars.id_scalar, scalars.datetime_scalar, diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py --- a/swh/graphql/backends/archive.py +++ b/swh/graphql/backends/archive.py @@ -4,6 +4,7 @@ # See top-level LICENSE file for more information from swh.graphql import server +from swh.model.swhids import ObjectType class Archive: @@ -43,9 +44,6 @@ def get_origin_snapshots(self, origin_url): return self.storage.origin_snapshot_get_all(origin_url) - def is_snapshot_available(self, snapshot_ids): - return not self.storage.snapshot_missing(snapshot_ids) - def get_snapshot_branches( self, snapshot, after=b"", first=50, target_types=[], name_include=None ): @@ -66,9 +64,6 @@ def get_releases(self, release_ids): return self.storage.release_get(releases=release_ids) - def is_directory_available(self, directory_ids): - return not self.storage.directory_missing(directory_ids) - def get_directory_entries(self, directory_id, after=None, first=50): return self.storage.directory_get_entries( directory_id, limit=first, page_token=after @@ -77,3 +72,13 @@ def get_content(self, content_id): # FIXME, only for tests return self.storage.content_find({"sha1_git": content_id}) + + def is_object_available(self, object_id, object_type) -> bool: + mapping = { + ObjectType.CONTENT: self.storage.content_missing, + ObjectType.DIRECTORY: self.storage.directory_missing, + ObjectType.RELEASE: self.storage.release_missing, + ObjectType.REVISION: self.storage.revision_missing, + ObjectType.SNAPSHOT: self.storage.snapshot_missing, + } + return not mapping[object_type]([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 @@ -35,12 +35,12 @@ """ def _get_node_data(self): - directory_id = self.kwargs.get("swhid").object_id + swhid = self.kwargs.get("swhid") # path = "" - if archive.Archive().is_directory_available([directory_id]): + if archive.Archive().is_object_available(swhid.object_id, swhid.object_type): # _get_directory_by_id is not making any backend call # hence the is_directory_available validation - return self._get_directory_by_id(directory_id) + return self._get_directory_by_id(swhid.object_id) return None 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 SearchSwhidConnection from .snapshot import ( OriginSnapshotConnection, SnapshotNode, @@ -50,6 +51,11 @@ "content": ContentNode, "dir-entry-dir": TargetDirectoryNode, "dir-entry-file": TargetContentNode, + "search-result-snapshot": TargetSnapshotNode, + "search-result-revision": TargetRevisionNode, + "search-result-release": TargetReleaseNode, + "search-result-directory": TargetDirectoryNode, + "search-result-content": TargetContentNode, } if resolver_type not in mapping: raise AttributeError(f"Invalid node type: {resolver_type}") @@ -67,6 +73,7 @@ "revision-parents": ParentRevisionConnection, "revision-log": LogRevisionConnection, "directory-entries": DirectoryEntryConnection, + "search-swhid": SearchSwhidConnection, } if resolver_type not in mapping: raise AttributeError(f"Invalid connection type: {resolver_type}") 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 @@ -34,11 +34,13 @@ release: ObjectType = ObjectType("Release") directory: ObjectType = ObjectType("Directory") directory_entry: ObjectType = ObjectType("DirectoryEntry") +search_result: ObjectType = ObjectType("SearchResult") binary_string: ObjectType = ObjectType("BinaryString") 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 @@ -172,6 +174,15 @@ return resolver(obj, info, **kw) +@search_result.field("target") +def search_result_target_resolver( + obj: rs.search.SearchResultNode, info: GraphQLResolveInfo, **kw +): + resolver_type = f"search-result-{obj.type}" + resolver = get_node_resolver(resolver_type) + return resolver(obj, info, **kw) + + # Connection resolvers # A connection resolver should return an instance of BaseConnection @@ -239,12 +250,19 @@ return resolver(obj, info, **kw) +@query.field("searchSwhid") +def search_swhid_resolver(obj, info: GraphQLResolveInfo, **kw): + resolver = get_connection_resolver("search-swhid") + return resolver(obj, info, **kw) + + # Any other type of resolver @release_target.type_resolver @directory_entry_target.type_resolver @branch_target.type_resolver +@search_result_target.type_resolver def union_resolver(obj, *_) -> str: """ Generic resolver for all the union types 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,33 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from swh.graphql.backends import archive +from swh.storage.interface import PagedResult + +from .base_connection import BaseConnection +from .base_node import BaseNode + + +class SearchResultNode(BaseNode): + """ """ + + +class SearchSwhidConnection(BaseConnection): + + _node_class = SearchResultNode + + def _get_paged_result(self): + swhid = self.kwargs.get("swhid") + results = [] + if archive.Archive().is_object_available(swhid.object_id, swhid.object_type): + results = [ + { + "target_hash": swhid.object_id, + "type": swhid.object_type.name.lower(), + # match will always be 100% for a SWHID lookup + "match": 100, + } + ] + return PagedResult(results=results) 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 @@ -40,9 +40,9 @@ def _get_node_data(self): """ """ - snapshot_id = self.kwargs.get("swhid").object_id - if archive.Archive().is_snapshot_available([snapshot_id]): - return self._get_snapshot_by_id(snapshot_id) + swhid = self.kwargs.get("swhid") + if archive.Archive().is_object_available(swhid.object_id, swhid.object_type): + return self._get_snapshot_by_id(swhid.object_id) return None 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 @@ -805,6 +805,68 @@ status: String } +""" +Connection to SearchResults +""" +type SearchResultConnection { + """ + List of SearchResult edges + """ + edges: [SearchResultEdge] + + """ + List of SearchResult objects + """ + nodes: [SearchResult] + + """ + Information for pagination + """ + pageInfo: PageInfo! + + """ + Total number of origin objects in the connection + """ + totalCount: Int +} + +""" +Edge in SearchResult connection +""" +type SearchResultEdge { + """ + Cursor to request the next page after the item + """ + cursor: String! + + """ + SearchResult object + """ + node: SearchResult +} + +union SearchResultTarget = Origin | Revision | Release | Content | Directory | Snapshot + +""" +A SearchResult object +""" +type SearchResult { + """ + Result target type + """ + type: String # FIXME, make this an enum type + + """ + Target object + """ + target: SearchResultTarget + + """ + Result match percentage to the search term + """ + match: Int +} + """ The query root of the GraphQL interface. """ @@ -903,4 +965,14 @@ """ swhid: SWHID! ): Content + + """ + Resolve the given SWHID to an object + """ + searchSwhid( + """ + SWHID to look for + """ + swhid: SWHID! + ): SearchResultConnection! }