Skip to content

Add support for custom global ID in v2 (Issue #1276) #1278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions graphene/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
Connection,
ConnectionField,
PageInfo,
BaseGlobalIDType,
DefaultGlobalIDType,
SimpleGlobalIDType,
UUIDGlobalIDType,
)
from .utils.resolve_only_args import resolve_only_args
from .utils.module_loading import lazy_import
Expand Down Expand Up @@ -85,6 +89,10 @@
"lazy_import",
"Context",
"ResolveInfo",
"BaseGlobalIDType",
"DefaultGlobalIDType",
"SimpleGlobalIDType",
"UUIDGlobalIDType",
# Deprecated
"AbstractType",
]
10 changes: 10 additions & 0 deletions graphene/relay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
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",
Expand All @@ -10,4 +16,8 @@
"Connection",
"ConnectionField",
"PageInfo",
"BaseGlobalIDType",
"DefaultGlobalIDType",
"SimpleGlobalIDType",
"UUIDGlobalIDType",
]
77 changes: 77 additions & 0 deletions graphene/relay/id_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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):
return from_global_id(global_id)

@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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will work if the return type is an interface or a union. I think it will return the interface/union type rather than the underling result type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are probably correct... I will add a unit test for that and try to figure out a solution.
Let me know if you have an idea to suggest

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't have a Union be a node so I don't think the union is really something we need to consider (unless I am missing your point), I'm looking into interfaces

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually same comment about interfaces... I don't really see why an interface would be a Node... it's the object type that you would derive from the interface that you would make a node field.
Did you have a specific case in mind? Cause I can't write a unit test that makes sense to test that...

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
47 changes: 31 additions & 16 deletions graphene/relay/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,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 @@ -27,8 +26,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 @@ -52,13 +61,13 @@ def __init__(self, node, type=False, deprecation_reason=None, name=None, **kwarg
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,
description="The ID of the object",
id=ID(required=True),
id=global_id_type.graphene_type(required=True),
)

def get_resolver(self, parent_resolver):
Expand All @@ -70,13 +79,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.global_id_type = global_id_type
_meta.fields = OrderedDict(
id=GlobalID(cls, description="The ID of the object.")
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 @@ -92,7 +111,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)
_type, _id = cls.resolve_global_id(info, global_id)
graphene_type = info.schema.get_type(_type).graphene_type
except Exception:
return None
Expand All @@ -110,10 +129,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