diff --git a/swh/graphql/app.py b/swh/graphql/app.py index 70554f4..e60acca 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,22 +1,24 @@ 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.visit, resolvers.visit_status, resolvers.snapshot, resolvers.snapshot_branch, resolvers.directory, + resolvers.directory_entry, resolvers.branch_target, + resolvers.directory_entry_target, scalars.datetime_scalar, scalars.swhid_scalar, scalars.sha1_scalar, scalars.binary_text_scalar, scalars.datetimezone_scalar, ) diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py index 105eb6e..dfd1c24 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,23 +1,37 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_node import BaseNode class BaseContentNode(BaseNode): def _get_content_by_id(self, content_id): return archive.Archive().get_content(content_id) @property def id(self): return b"test" + def is_type_of(self): + return "Content" + class ContentNode(BaseContentNode): def _get_node_data(self): """ When a content is requested directly (not from a connection) with an id """ content_id = utils.str_to_swid(self.kwargs.get("SWHID")) return self._get_content_by_id(content_id)[0] + + +class DirectoryEntryContentNode(BaseContentNode): + def _get_node_data(self): + """ + When a content is requested from a + directory entry + content id is obj.target here + """ + content_id = self.obj.target + return self._get_content_by_id(content_id) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py index d476267..0ca19f9 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,33 +1,49 @@ from swh.graphql.utils import utils from .base_node import BaseNode class BaseDirectoryNode(BaseNode): def _get_directory_by_id(self, directory_id): # Now not fetching any data (schema is exposing just id) # same pattern is used in snapshot resolver # FIXME, use the right API to fetch metadata like name, path return { "id": directory_id, } + def is_type_of(self): + return "Directory" + class DirectoryNode(BaseDirectoryNode): def _get_node_data(self): """ When a directory is requested directly (not from a connection) with an id """ directory_id = utils.str_to_swid(self.kwargs.get("Sha1")) # path = "" return self._get_directory_by_id(directory_id) class RevisionDirectoryNode(BaseDirectoryNode): def _get_node_data(self): """ When a directory is requested from a revision + self.obj is revision here + self.obj.directoryId is the required dir id + (set from resolvers.revision.py:BaseRevisionNode) """ directory_id = self.kwargs.get("sha1") return self._get_directory_by_id(directory_id) + + +class DirectoryEntryDirectoryNode(BaseDirectoryNode): + def _get_node_data(self): + """ + When a sub directory is requested from a + parent directory entry + obj.target is the sub directory id here + """ + return self._get_directory_by_id(self.obj.target) diff --git a/swh/graphql/resolvers/directory_entry.py b/swh/graphql/resolvers/directory_entry.py index b1a31dc..e65d987 100644 --- a/swh/graphql/resolvers/directory_entry.py +++ b/swh/graphql/resolvers/directory_entry.py @@ -1,32 +1,32 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode class DirectoryEntryNode(BaseNode): """ """ @property def targetId(self): # To support the schema naming convention return self._node.target class DirectoryEntryConnection(BaseConnection): _node_class = DirectoryEntryNode def _get_paged_result(self): """ When entries requested from a directory self.obj.id is directory_id here (as returned from resolvers/directory.py) This is not paginated from swh-storgae using dummy pagination """ - # FIXME, using dummy(local) pagination, move to backend + # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call entries = archive.Archive().get_directory_entries(self.obj.id) return utils.paginated(entries, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/resolvers/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py index eeda891..5f13ed8 100644 --- a/swh/graphql/resolvers/resolver_factory.py +++ b/swh/graphql/resolvers/resolver_factory.py @@ -1,54 +1,58 @@ -from .content import ContentNode -from .directory import DirectoryNode +from .content import ContentNode, DirectoryEntryContentNode +from .directory import DirectoryEntryDirectoryNode, DirectoryNode from .directory_entry import DirectoryEntryConnection from .origin import OriginConnection, OriginNode 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, "directory": DirectoryNode, "content": ContentNode, + "dir-entry-dir": DirectoryEntryDirectoryNode, + "dir-entry-file": DirectoryEntryContentNode, + # revision-directory } # 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, "directory-entries": DirectoryEntryConnection, + # revision-parents } # 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 24da7d3..5817523 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,130 +1,143 @@ """ 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 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") visit = ObjectType("Visit") visit_status = ObjectType("VisitStatus") snapshot = ObjectType("Snapshot") snapshot_branch = ObjectType("Branch") directory = ObjectType("Directory") +directory_entry = ObjectType("DirectoryEntry") branch_target = UnionType("BranchTarget") +directory_entry_target = UnionType("DirectoryEntryTarget") # Node resolvers # 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)() @visit_status.field("snapshot") def visit_snapshot_resolver(obj, info, **kw): resolver = get_node_resolver("visit-snapshot") return resolver(obj, info, **kw)() @snapshot_branch.field("target") def snapshot_branch_target_resolver(obj, info, **kw): """ Snapshot 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)() @query.field("directory") def directory_resolver(obj, info, **kw): resolver = get_node_resolver("directory") return resolver(obj, info, **kw)() +@directory_entry.field("target") +def directory_entry_target_resolver(obj, info, **kw): + """ + directory entry target can be a directory or a content + """ + resolver_type = f"dir-entry-{obj.type}" + resolver = get_node_resolver(resolver_type) + return resolver(obj, info, **kw)() + + @query.field("content") def content_resolver(obj, info, **kw): resolver = get_node_resolver("content") 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_resolver(obj, info, **kw): resolver = get_connection_resolver("snapshot-branches") return resolver(obj, info, **kw)() @directory.field("entries") def directory_entry_resolver(obj, info, **kw): resolver = get_connection_resolver("directory-entries") return resolver(obj, info, **kw)() # Any other type of resolver +@directory_entry_target.type_resolver @branch_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 feac370..be39e8f 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,100 +1,100 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_node import BaseNode from .directory import RevisionDirectoryNode 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 - # change to node factory + # Storage fix or use local paginator + # Move to resolvers.resolvers.py - # FIXME, now making one db calls per parent - # Change to get the nodedata list here itself return [ ParentRevisionNode(obj=self, info=self.info, sha1=revision_id) for revision_id in self.parentIds ] @property def directoryId(self): # To support the schema naming convention """ """ return self._node.directory @property def directory(self): """ Return the """ # FIXME change to node factory return RevisionDirectoryNode(obj=self, info=self.info, sha1=self.directoryId) 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("Sha1")) return self._get_revision_by_id(revision_id) class ParentRevisionNode(BaseRevisionNode): """ When a parent revision is requested + from a revision """ + # This class will be removed onece ParentRevisionConnection + # is implemented 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 """ return self._get_revision_by_id(self.obj.target) diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index ddc642f..45624b6 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,303 +1,303 @@ scalar SWHID scalar Sha1 scalar DateTime scalar DateTimeZone scalar BinaryText interface Node { id: ID! } interface SWHIDNode { id: SWHID! } interface SWHNode { id: Sha1! } 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: Sha1! 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 { status: String! date: DateTime! snapshot: Snapshot type: String } # FIXME, add OriginSnapshotConnection type Snapshot implements SWHNode { id: Sha1! 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 can be Content, Directory, Snapshot, or Alias as well union BranchTarget = Revision | Release type Branch { 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: Sha1! message: BinaryText author: Person committer: Person date: DateTimeZone type: String directoryId: Sha1 directory: Directory parentIds: [Sha1] parents: [Revision] } # type ReleaseConnection { # } # type ReleasEdge { # } type Release implements SWHNode { id: Sha1! name: BinaryText message: BinaryText author: Person date: DateTimeZone } type DirectoryEntryConnection { edges: [DirectoryEntryEdge] nodes: [DirectoryEntry] pageInfo: PageInfo! totalCount: Int } type DirectoryEntryEdge { cursor: String! node: DirectoryEntry } -union DirectoryTarget = Directory | Content +union DirectoryEntryTarget = Directory | Content type DirectoryEntry { name: BinaryText type: String # FIXME, replace with enum targetId: Sha1 - target: DirectoryTarget + target: DirectoryEntryTarget } type Directory implements SWHNode { id: Sha1! entries( first: Int after: String ): DirectoryEntryConnection } type ContentChecksum { test: String } type ContentType { test: String } type ContentLanguage { test: String } type ContentLicense { test: String } -type Content implements SWHIDNode { - id: SWHID! +type Content implements SWHNode { + id: Sha1! checksum: ContentChecksum # data: filetype: ContentType language: ContentLanguage length: Int license: ContentLicense 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( Sha1: 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( Sha1: String! ): Revision """ Get the release with the given swhid """ release( Sha1: String! ): Release """ Get the directory with the given swhid """ directory( Sha1: String! ): Directory """ Get the content with the given swhid """ content( SWHID: String! ): Content # """ # Search with the given swhid # """ # searchWithSwhid }