Repository: mirumee/ariadne
Pull request #1227 (2025-02-04 21:07:20)

feat!: add a default global ID encoder, add a id_field on the query type
BREAKING CHANGE: the global ID function takes only a str with the GID, the query type uses the `id_field` arg to determine which kwarg to pass to the decoder

One would be able to do:

```python
query = RelayQueryType(
    id_field="bid",
)
```
Instead of this:

```python
def decode_global_id(kwargs) -> GlobalIDTuple:
    return GlobalIDTuple(*b64decode(kwargs["bid"]).decode().split(":"))


query = RelayQueryType(
    global_id_decoder=decode_global_id,
)
```

With the following: https://github.com/mirumee/ariadne/blob/2c8f064c4538bd26e709ed8da665c2236fcbb906/ariadne/contrib/relay/objects.py#L122

This is to make the decoder a bit dummer as to take the responsibility of pulling the GID from the right kwarg (or query param, if you will) from the decoder and leave it with the single responsibility. 


Patch:
diff --git a/ariadne/contrib/relay/__init__.py b/ariadne/contrib/relay/__init__.py
index 07b0469..79166e0 100644
--- a/ariadne/contrib/relay/__init__.py
+++ b/ariadne/contrib/relay/__init__.py
@@ -8,6 +8,7 @@ from ariadne.contrib.relay.objects import (
     RelayQueryType,
 )
 from ariadne.contrib.relay.types import ConnectionResolver, GlobalIDTuple
+from ariadne.contrib.relay.utils import decode_global_id, encode_global_id
 
 __all__ = [
     "ConnectionArguments",
@@ -17,4 +18,6 @@ __all__ = [
     "RelayQueryType",
     "ConnectionResolver",
     "GlobalIDTuple",
+    "decode_global_id",
+    "encode_global_id",
 ]
diff --git a/ariadne/contrib/relay/connection.py b/ariadne/contrib/relay/connection.py
index 9ed8f54..4f69e55 100644
--- a/ariadne/contrib/relay/connection.py
+++ b/ariadne/contrib/relay/connection.py
@@ -1,6 +1,4 @@
-from typing import Sequence
-
-from typing_extensions import Any
+from typing import Sequence, Any
 
 from ariadne.contrib.relay.arguments import ConnectionArgumentsUnion
 
@@ -12,14 +10,19 @@ class RelayConnection:
         total: int,
         has_next_page: bool,
         has_previous_page: bool,
+        id_field: str = "id",
     ) -> None:
         self.edges = edges
         self.total = total
         self.has_next_page = has_next_page
         self.has_previous_page = has_previous_page
+        self.id_field = id_field
+
+    def get_cursor(self, obj):
+        return obj[self.id_field]
 
-    def get_cursor(self, node):
-        return node["id"]
+    def get_node(self, obj):
+        return obj
 
     def get_page_info(
         self, connection_arguments: ConnectionArgumentsUnion
@@ -32,4 +35,7 @@ class RelayConnection:
         }
 
     def get_edges(self):
-        return [{"node": node, "cursor": self.get_cursor(node)} for node in self.edges]
+        return [
+            {"node": self.get_node(obj), "cursor": self.get_cursor(obj)}
+            for obj in self.edges
+        ]
diff --git a/ariadne/contrib/relay/objects.py b/ariadne/contrib/relay/objects.py
index 674b218..3e93509 100644
--- a/ariadne/contrib/relay/objects.py
+++ b/ariadne/contrib/relay/objects.py
@@ -1,4 +1,3 @@
-from base64 import b64decode
 from inspect import iscoroutinefunction
 from typing import Optional, Tuple, cast
 
@@ -14,17 +13,13 @@ from ariadne.contrib.relay.arguments import (
 from ariadne.contrib.relay.types import (
     ConnectionResolver,
     GlobalIDDecoder,
-    GlobalIDTuple,
 )
+from ariadne.contrib.relay.utils import decode_global_id
 from ariadne.types import Resolver
 from ariadne.utils import type_get_extension
 from ariadne.utils import type_set_extension
 
 
-def decode_global_id(kwargs) -> GlobalIDTuple:
-    return GlobalIDTuple(*b64decode(kwargs["id"]).decode().split(":"))
-
-
 class RelayObjectType(ObjectType):
     _node_resolver: Optional[Resolver] = None
 
@@ -93,6 +88,7 @@ class RelayObjectType(ObjectType):
 
 
 class RelayNodeInterfaceType(InterfaceType):
+
     def __init__(
         self,
         type_resolver: Optional[Resolver] = None,
@@ -105,6 +101,7 @@ class RelayQueryType(RelayObjectType):
         self,
         node: Optional[RelayNodeInterfaceType] = None,
         global_id_decoder: GlobalIDDecoder = decode_global_id,
+        id_field: str = "id",
     ) -> None:
         super().__init__("Query")
         if node is None:
@@ -112,6 +109,7 @@ class RelayQueryType(RelayObjectType):
         self.node = node
         self.set_field("node", self.resolve_node)
         self.global_id_decoder = global_id_decoder
+        self.id_field = id_field
 
     @property
     def bindables(self) -> Tuple["RelayQueryType", "RelayNodeInterfaceType"]:
@@ -127,7 +125,7 @@ class RelayQueryType(RelayObjectType):
         return resolver
 
     def resolve_node(self, obj, info, *args, **kwargs):
-        type_name, _ = self.global_id_decoder(kwargs)
+        type_name, _ = self.global_id_decoder(kwargs[self.id_field])
 
         resolver = self.get_node_resolver(type_name, info.schema)
 
diff --git a/ariadne/contrib/relay/types.py b/ariadne/contrib/relay/types.py
index 8a1c3aa..6e9e20a 100644
--- a/ariadne/contrib/relay/types.py
+++ b/ariadne/contrib/relay/types.py
@@ -1,5 +1,5 @@
 from collections import namedtuple
-from typing import Any, Callable, Dict
+from typing import Callable
 
 from typing_extensions import TypeVar
 
@@ -7,4 +7,5 @@ from ariadne.contrib.relay.connection import RelayConnection
 
 ConnectionResolver = TypeVar("ConnectionResolver", bound=Callable[..., RelayConnection])
 GlobalIDTuple = namedtuple("GlobalIDTuple", ["type", "id"])
-GlobalIDDecoder = Callable[[Dict[str, Any]], GlobalIDTuple]
+GlobalIDDecoder = Callable[[str], GlobalIDTuple]
+GlobalIDEncoder = Callable[[str, str], str]
diff --git a/ariadne/contrib/relay/utils.py b/ariadne/contrib/relay/utils.py
new file mode 100644
index 0000000..0105dba
--- /dev/null
+++ b/ariadne/contrib/relay/utils.py
@@ -0,0 +1,13 @@
+from base64 import b64decode, b64encode
+
+from ariadne.contrib.relay.types import (
+    GlobalIDTuple,
+)
+
+
+def decode_global_id(gid: str) -> GlobalIDTuple:
+    return GlobalIDTuple(*b64decode(gid).decode().split(":"))
+
+
+def encode_global_id(type_name: str, _id: str) -> str:
+    return b64encode(f"{type_name}:{_id}".encode()).decode()
diff --git a/pyproject.toml b/pyproject.toml
index f8d545a..56250db 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "ariadne"
-version = "0.25.1"
+version = "0.25.2"
 description = "Ariadne is a Python library for implementing GraphQL servers."
 authors = [{ name = "Mirumee Software", email = "hello@mirumee.com" }]
 readme = "README.md"


Test patch:
diff --git a/tests/relay/conftest.py b/tests/relay/conftest.py
index 17549b3..fbf7859 100644
--- a/tests/relay/conftest.py
+++ b/tests/relay/conftest.py
@@ -58,7 +58,7 @@ type Query {
 
 @pytest.fixture
 def global_id_decoder():
-    return lambda kwargs: GlobalIDTuple(*b64decode(kwargs["bid"]).decode().split(":"))
+    return lambda gid: GlobalIDTuple(*b64decode(gid).decode().split(":"))
 
 
 @pytest.fixture
@@ -71,6 +71,7 @@ def relay_query(factions, relay_node_interface, global_id_decoder):
     query = RelayQueryType(
         node=relay_node_interface,
         global_id_decoder=global_id_decoder,
+        id_field="bid",
     )
     query.set_field("rebels", lambda *_: factions[0])
     query.set_field("empire", lambda *_: factions[1])
diff --git a/tests/relay/test_objects.py b/tests/relay/test_objects.py
index e8c2ca5..cd96d5e 100644
--- a/tests/relay/test_objects.py
+++ b/tests/relay/test_objects.py
@@ -1,4 +1,5 @@
 import pytest
+
 from graphql import extend_schema
 from graphql import graphql_sync
 from graphql import parse
@@ -15,9 +16,6 @@ from ariadne.contrib.relay.objects import (
     RelayQueryType,
     decode_global_id,
 )
-from ariadne.contrib.relay.types import (
-    GlobalIDTuple,
-)
 
 
 @pytest.fixture
@@ -31,10 +29,6 @@ def friends_connection():
     )
 
 
-def test_decode_global_id():
-    assert decode_global_id({"id": "VXNlcjox"}) == GlobalIDTuple("User", "1")
-
-
 def test_default_id_decoder():
     query = RelayQueryType()
     assert query.global_id_decoder is decode_global_id
diff --git a/tests/relay/test_utils.py b/tests/relay/test_utils.py
new file mode 100644
index 0000000..a411ced
--- /dev/null
+++ b/tests/relay/test_utils.py
@@ -0,0 +1,13 @@
+from ariadne.contrib.relay import (
+    GlobalIDTuple,
+    decode_global_id,
+    encode_global_id,
+)
+
+
+def test_decode_global_id():
+    assert decode_global_id("VXNlcjox") == GlobalIDTuple("User", "1")
+
+
+def test_encode_global_id():
+    assert encode_global_id("User", "1") == "VXNlcjox"