Skip to content

Commit 4a057f2

Browse files
author
Thomas Leonard
committed
feat: Add support for custom global ID
1 parent efe4b89 commit 4a057f2

File tree

5 files changed

+362
-29
lines changed

5 files changed

+362
-29
lines changed

graphene/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from .pyutils.version import get_version
22
from .relay import (
3+
BaseGlobalIDType,
34
ClientIDMutation,
45
Connection,
56
ConnectionField,
7+
DefaultGlobalIDType,
68
GlobalID,
79
Node,
810
PageInfo,
11+
SimpleGlobalIDType,
12+
UUIDGlobalIDType,
913
is_node,
1014
)
1115
from .types import (
@@ -50,6 +54,7 @@
5054
"__version__",
5155
"Argument",
5256
"Base64",
57+
"BaseGlobalIDType",
5358
"Boolean",
5459
"ClientIDMutation",
5560
"Connection",
@@ -58,6 +63,7 @@
5863
"Date",
5964
"DateTime",
6065
"Decimal",
66+
"DefaultGlobalIDType",
6167
"Dynamic",
6268
"Enum",
6369
"Field",
@@ -78,10 +84,12 @@
7884
"ResolveInfo",
7985
"Scalar",
8086
"Schema",
87+
"SimpleGlobalIDType",
8188
"String",
8289
"Time",
83-
"UUID",
8490
"Union",
91+
"UUID",
92+
"UUIDGlobalIDType",
8593
"is_node",
8694
"lazy_import",
8795
"resolve_only_args",

graphene/relay/__init__.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
from .node import Node, is_node, GlobalID
22
from .mutation import ClientIDMutation
33
from .connection import Connection, ConnectionField, PageInfo
4+
from .id_type import (
5+
BaseGlobalIDType,
6+
DefaultGlobalIDType,
7+
SimpleGlobalIDType,
8+
UUIDGlobalIDType,
9+
)
410

511
__all__ = [
6-
"Node",
7-
"is_node",
8-
"GlobalID",
12+
"BaseGlobalIDType",
913
"ClientIDMutation",
1014
"Connection",
1115
"ConnectionField",
16+
"DefaultGlobalIDType",
17+
"GlobalID",
18+
"Node",
1219
"PageInfo",
20+
"SimpleGlobalIDType",
21+
"UUIDGlobalIDType",
22+
"is_node",
1323
]

graphene/relay/id_type.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from graphql_relay import from_global_id, to_global_id
2+
3+
from ..types import ID, UUID
4+
from ..types.base import BaseType
5+
6+
from typing import Type
7+
8+
9+
class BaseGlobalIDType:
10+
"""
11+
Base class that define the required attributes/method for a type.
12+
"""
13+
14+
graphene_type = ID # type: Type[BaseType]
15+
16+
@classmethod
17+
def resolve_global_id(cls, info, global_id):
18+
# return _type, _id
19+
raise NotImplementedError
20+
21+
@classmethod
22+
def to_global_id(cls, _type, _id):
23+
# return _id
24+
raise NotImplementedError
25+
26+
27+
class DefaultGlobalIDType(BaseGlobalIDType):
28+
"""
29+
Default global ID type: base64 encoded version of "<node type name>: <node id>".
30+
"""
31+
32+
graphene_type = ID
33+
34+
@classmethod
35+
def resolve_global_id(cls, info, global_id):
36+
try:
37+
_type, _id = from_global_id(global_id)
38+
if not _type:
39+
raise ValueError("Invalid Global ID")
40+
return _type, _id
41+
except Exception as e:
42+
raise Exception(
43+
f'Unable to parse global ID "{global_id}". '
44+
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
45+
f"Exception message: {e}"
46+
)
47+
48+
@classmethod
49+
def to_global_id(cls, _type, _id):
50+
return to_global_id(_type, _id)
51+
52+
53+
class SimpleGlobalIDType(BaseGlobalIDType):
54+
"""
55+
Simple global ID type: simply the id of the object.
56+
To be used carefully as the user is responsible for ensuring that the IDs are indeed global
57+
(otherwise it could cause request caching issues).
58+
"""
59+
60+
graphene_type = ID
61+
62+
@classmethod
63+
def resolve_global_id(cls, info, global_id):
64+
_type = info.return_type.graphene_type._meta.name
65+
return _type, global_id
66+
67+
@classmethod
68+
def to_global_id(cls, _type, _id):
69+
return _id
70+
71+
72+
class UUIDGlobalIDType(BaseGlobalIDType):
73+
"""
74+
UUID global ID type.
75+
By definition UUID are global so they are used as they are.
76+
"""
77+
78+
graphene_type = UUID
79+
80+
@classmethod
81+
def resolve_global_id(cls, info, global_id):
82+
_type = info.return_type.graphene_type._meta.name
83+
return _type, global_id
84+
85+
@classmethod
86+
def to_global_id(cls, _type, _id):
87+
return _id

graphene/relay/node.py

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from functools import partial
22
from inspect import isclass
33

4-
from graphql_relay import from_global_id, to_global_id
5-
6-
from ..types import ID, Field, Interface, ObjectType
4+
from ..types import Field, Interface, ObjectType
75
from ..types.interface import InterfaceOptions
86
from ..types.utils import get_type
7+
from .id_type import BaseGlobalIDType, DefaultGlobalIDType
98

109

1110
def is_node(objecttype):
@@ -22,8 +21,18 @@ def is_node(objecttype):
2221

2322

2423
class GlobalID(Field):
25-
def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs):
26-
super(GlobalID, self).__init__(ID, required=required, *args, **kwargs)
24+
def __init__(
25+
self,
26+
node=None,
27+
parent_type=None,
28+
required=True,
29+
global_id_type=DefaultGlobalIDType,
30+
*args,
31+
**kwargs,
32+
):
33+
super(GlobalID, self).__init__(
34+
global_id_type.graphene_type, required=required, *args, **kwargs
35+
)
2736
self.node = node or Node
2837
self.parent_type_name = parent_type._meta.name if parent_type else None
2938

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

5161
super(NodeField, self).__init__(
52-
# If we don's specify a type, the field type will be the node
53-
# interface
62+
# If we don't specify a type, the field type will be the node interface
5463
type_ or node,
55-
id=ID(required=True, description="The ID of the object"),
64+
id=global_id_type.graphene_type(
65+
required=True, description="The ID of the object"
66+
),
5667
**kwargs,
5768
)
5869

@@ -65,11 +76,23 @@ class Meta:
6576
abstract = True
6677

6778
@classmethod
68-
def __init_subclass_with_meta__(cls, **options):
79+
def __init_subclass_with_meta__(cls, global_id_type=DefaultGlobalIDType, **options):
80+
assert issubclass(
81+
global_id_type, BaseGlobalIDType
82+
), "Custom ID type need to be implemented as a subclass of BaseGlobalIDType."
6983
_meta = InterfaceOptions(cls)
70-
_meta.fields = {"id": GlobalID(cls, description="The ID of the object")}
84+
_meta.global_id_type = global_id_type
85+
_meta.fields = {
86+
"id": GlobalID(
87+
cls, global_id_type=global_id_type, description="The ID of the object"
88+
)
89+
}
7190
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
7291

92+
@classmethod
93+
def resolve_global_id(cls, info, global_id):
94+
return cls._meta.global_id_type.resolve_global_id(info, global_id)
95+
7396

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

85108
@classmethod
86109
def get_node_from_global_id(cls, info, global_id, only_type=None):
87-
try:
88-
_type, _id = cls.from_global_id(global_id)
89-
if not _type:
90-
raise ValueError("Invalid Global ID")
91-
except Exception as e:
92-
raise Exception(
93-
f'Unable to parse global ID "{global_id}". '
94-
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
95-
f"Exception message: {e}"
96-
)
110+
_type, _id = cls.resolve_global_id(info, global_id)
97111

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

119-
@classmethod
120-
def from_global_id(cls, global_id):
121-
return from_global_id(global_id)
122-
123133
@classmethod
124134
def to_global_id(cls, type_, id):
125-
return to_global_id(type_, id)
135+
return cls._meta.global_id_type.to_global_id(type_, id)

0 commit comments

Comments
 (0)