Skip to content

Commit

Permalink
feat: Add support for custom global (Issue #1276) (#1428)
Browse files Browse the repository at this point in the history
Co-authored-by: Thomas Leonard <[email protected]>
  • Loading branch information
tcleonard and Thomas Leonard authored Sep 19, 2022
1 parent b20bbdc commit ee1ff97
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
if: ${{ matrix.python == '3.10' }}
uses: actions/upload-artifact@v3
with:
name: graphene-sqlalchemy-coverage
name: graphene-coverage
path: coverage.xml
if-no-files-found: error
- name: Upload coverage.xml to codecov
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ help:
install-dev:
pip install -e ".[dev]"

.PHONY: test ## Run tests
test:
py.test graphene examples

Expand Down
10 changes: 9 additions & 1 deletion graphene/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from .pyutils.version import get_version
from .relay import (
BaseGlobalIDType,
ClientIDMutation,
Connection,
ConnectionField,
DefaultGlobalIDType,
GlobalID,
Node,
PageInfo,
SimpleGlobalIDType,
UUIDGlobalIDType,
is_node,
)
from .types import (
Expand Down Expand Up @@ -52,6 +56,7 @@
"Argument",
"Base64",
"BigInt",
"BaseGlobalIDType",
"Boolean",
"ClientIDMutation",
"Connection",
Expand All @@ -60,6 +65,7 @@
"Date",
"DateTime",
"Decimal",
"DefaultGlobalIDType",
"Dynamic",
"Enum",
"Field",
Expand All @@ -80,10 +86,12 @@
"ResolveInfo",
"Scalar",
"Schema",
"SimpleGlobalIDType",
"String",
"Time",
"UUID",
"Union",
"UUID",
"UUIDGlobalIDType",
"is_node",
"lazy_import",
"resolve_only_args",
Expand Down
16 changes: 13 additions & 3 deletions graphene/relay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
from .node import Node, is_node, GlobalID
from .mutation import ClientIDMutation
from .connection import Connection, ConnectionField, PageInfo
from .id_type import (
BaseGlobalIDType,
DefaultGlobalIDType,
SimpleGlobalIDType,
UUIDGlobalIDType,
)

__all__ = [
"Node",
"is_node",
"GlobalID",
"BaseGlobalIDType",
"ClientIDMutation",
"Connection",
"ConnectionField",
"DefaultGlobalIDType",
"GlobalID",
"Node",
"PageInfo",
"SimpleGlobalIDType",
"UUIDGlobalIDType",
"is_node",
]
87 changes: 87 additions & 0 deletions graphene/relay/id_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from graphql_relay import from_global_id, to_global_id

from ..types import ID, UUID
from ..types.base import BaseType

from typing import Type


class BaseGlobalIDType:
"""
Base class that define the required attributes/method for a type.
"""

graphene_type = ID # type: Type[BaseType]

@classmethod
def resolve_global_id(cls, info, global_id):
# return _type, _id
raise NotImplementedError

@classmethod
def to_global_id(cls, _type, _id):
# return _id
raise NotImplementedError


class DefaultGlobalIDType(BaseGlobalIDType):
"""
Default global ID type: base64 encoded version of "<node type name>: <node id>".
"""

graphene_type = ID

@classmethod
def resolve_global_id(cls, info, global_id):
try:
_type, _id = from_global_id(global_id)
if not _type:
raise ValueError("Invalid Global ID")
return _type, _id
except Exception as e:
raise Exception(
f'Unable to parse global ID "{global_id}". '
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
f"Exception message: {e}"
)

@classmethod
def to_global_id(cls, _type, _id):
return to_global_id(_type, _id)


class SimpleGlobalIDType(BaseGlobalIDType):
"""
Simple global ID type: simply the id of the object.
To be used carefully as the user is responsible for ensuring that the IDs are indeed global
(otherwise it could cause request caching issues).
"""

graphene_type = ID

@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id

@classmethod
def to_global_id(cls, _type, _id):
return _id


class UUIDGlobalIDType(BaseGlobalIDType):
"""
UUID global ID type.
By definition UUID are global so they are used as they are.
"""

graphene_type = UUID

@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id

@classmethod
def to_global_id(cls, _type, _id):
return _id
60 changes: 35 additions & 25 deletions graphene/relay/node.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from functools import partial
from inspect import isclass

from graphql_relay import from_global_id, to_global_id

from ..types import ID, Field, Interface, ObjectType
from ..types import Field, Interface, ObjectType
from ..types.interface import InterfaceOptions
from ..types.utils import get_type
from .id_type import BaseGlobalIDType, DefaultGlobalIDType


def is_node(objecttype):
Expand All @@ -22,8 +21,18 @@ def is_node(objecttype):


class GlobalID(Field):
def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs):
super(GlobalID, self).__init__(ID, required=required, *args, **kwargs)
def __init__(
self,
node=None,
parent_type=None,
required=True,
global_id_type=DefaultGlobalIDType,
*args,
**kwargs,
):
super(GlobalID, self).__init__(
global_id_type.graphene_type, required=required, *args, **kwargs
)
self.node = node or Node
self.parent_type_name = parent_type._meta.name if parent_type else None

Expand All @@ -47,12 +56,14 @@ def __init__(self, node, type_=False, **kwargs):
assert issubclass(node, Node), "NodeField can only operate in Nodes"
self.node_type = node
self.field_type = type_
global_id_type = node._meta.global_id_type

super(NodeField, self).__init__(
# If we don's specify a type, the field type will be the node
# interface
# If we don't specify a type, the field type will be the node interface
type_ or node,
id=ID(required=True, description="The ID of the object"),
id=global_id_type.graphene_type(
required=True, description="The ID of the object"
),
**kwargs,
)

Expand All @@ -65,11 +76,23 @@ class Meta:
abstract = True

@classmethod
def __init_subclass_with_meta__(cls, **options):
def __init_subclass_with_meta__(cls, global_id_type=DefaultGlobalIDType, **options):
assert issubclass(
global_id_type, BaseGlobalIDType
), "Custom ID type need to be implemented as a subclass of BaseGlobalIDType."
_meta = InterfaceOptions(cls)
_meta.fields = {"id": GlobalID(cls, description="The ID of the object")}
_meta.global_id_type = global_id_type
_meta.fields = {
"id": GlobalID(
cls, global_id_type=global_id_type, description="The ID of the object"
)
}
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)

@classmethod
def resolve_global_id(cls, info, global_id):
return cls._meta.global_id_type.resolve_global_id(info, global_id)


class Node(AbstractNode):
"""An object with an ID"""
Expand All @@ -84,16 +107,7 @@ def node_resolver(cls, only_type, root, info, id):

@classmethod
def get_node_from_global_id(cls, info, global_id, only_type=None):
try:
_type, _id = cls.from_global_id(global_id)
if not _type:
raise ValueError("Invalid Global ID")
except Exception as e:
raise Exception(
f'Unable to parse global ID "{global_id}". '
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
f"Exception message: {e}"
)
_type, _id = cls.resolve_global_id(info, global_id)

graphene_type = info.schema.get_type(_type)
if graphene_type is None:
Expand All @@ -116,10 +130,6 @@ def get_node_from_global_id(cls, info, global_id, only_type=None):
if get_node:
return get_node(info, _id)

@classmethod
def from_global_id(cls, global_id):
return from_global_id(global_id)

@classmethod
def to_global_id(cls, type_, id):
return to_global_id(type_, id)
return cls._meta.global_id_type.to_global_id(type_, id)
Loading

0 comments on commit ee1ff97

Please sign in to comment.