Description
Note
This issue is a duplicate of #971 but includes a full description for searchability and links to history on the tracker itself.
What is the Current Behavior?
Assume a fixed schema with two (or more) different GraphQL object types using graphene_django.DjangoObjectType
linked to the same Django model:
import graphene_django
from .models import Org as OrgModel
class Org(graphene_django.DjangoObjectType):
class Meta:
model = OrgModel
fields = (
"id",
"name",
"billing"
)
class AnonymousOrg(graphene_django.DjangoObjectType):
class Meta:
model = OrgModel
fields = (
"id",
"name",
)
Assume a query to Org
of ID 7eca71ed-ff04-4473-9fd1-0a587705f885
.
btoa('Org:7eca71ed-ff04-4473-9fd1-0a587705f885')
'T3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ=='
{
node(id: "T3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ==") {
id
__typename
... on Org {
id
}
}
}
Response (incorrect):
{
"data": {
"node": {
"id": "QW5vbnltb3VzT3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ==",
"__typename": "AnonymousOrg"
}
}
}
It returns the other object type 'AnonymousOrg:7eca71ed-ff04-4473-9fd1-0a587705f885'
, despite the relay ID specifying it was an Org
object.
What is the Expected Behavior?
Should return the object type specified in the relay ID.
Return (expected):
{
"data": {
"node": {
"id": "T3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ==",
"__typename": "Org"
}
}
}
Motivation / Use Case for Changing the Behavior
- For
node(id: "")
based queries to handle object types based on the same Django model. - To resolve miscommunication and confusion between other issues and StackOverflow.
Environment
- Version: 2.4.0
- Platform: graphene 2.1.4
History
-
May 24, 2020: Issue DjangoObjectType duplicate models breaks Relay node resolution #971 posted just linking a complete description. While it's good to recreate it, the lack of description effectively made it unsearchable to many trying to look it up and hidden (StackOverflow posts and comments are being made and none of them cite any bug).
-
Feb 2, 2017: PR Allow nodes to skip the registry #104 by @Tritlo.
-
Feb 6, 2017: Bug reported by @nickhudkins DjangoObjectType duplicate models breaks Relay node resolution #107.
-
Feb 12, 2017: DjangoObjectType duplicate models breaks Relay node resolution #107 closed by @syrusakbary:
Right now you can make this work with using a new registry for the second definition.
from graphene_django.registry import Registry class ThingB(DjangoObjectType): class Meta: registry = Registry()
Also, this issue Allow nodes to skip the registry #104 might be related :)
-
Feb 20, 2017: Replaced by Improved the global registry #115 by @syrusakbary:
Merged to master c635db5.
However, no history of it remains in trunk. It seems to have been rebased out of master without any revert or explanation: docs/registry.rst is removed.
It's not clear what the registry does, but it looks like different issues are being convoluted with this one.
When a relay ID is passed, it should return the object of the type encoded in the ID, e.g.
btoa('Org:7eca71ed-ff04-4473-9fd1-0a587705f885') 'T3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ=='
This would return the GraphQL type
Org
. But instead it's not deterministic, it will return any GraphQL object type using the same model, and disregard the object type.
Other
- StackOverflow question: https://stackoverflow.com/questions/70826464/graphene-django-determine-object-type-when-multiple-graphql-object-types-use-th
Workaround
Graphene 2
Version 1
@boolangery posted a workaround on May 25, 2020:
class FixRelayNodeResolutionMixin:
@classmethod
def get_node(cls, info, pk):
instance = super(FixRelayNodeResolutionMixin, cls).get_node(info, pk)
setattr(instance, "graphql_type", cls.__name__)
return instance
@classmethod
def is_type_of(cls, root, info):
if hasattr(root, "graphql_type"):
return getattr(root, "graphql_type") == cls.__name__
return super(FixRelayNodeResolutionMixin, cls).is_type_of(root, info)
class PublicUserType(FixRelayNodeResolutionMixin, DjangoObjectType):
class Meta:
model = User
interfaces = (graphene.relay.Node,)
fields = ['id', 'first_name', 'last_name']
class UserType(FixRelayNodeResolutionMixin, DjangoObjectType):
class Meta:
model = User
interfaces = (graphene.relay.Node,)
fields = ['id', 'first_name', 'last_name', 'profile']
Version 2
ass FixRelayNodeResolutionMixin:
"""
Fix issue where DjangoObjectType using same model aren't returned in node(id: )
WARNING: This needs to be listed _before_ SecureDjangoObjectType when inherited.
Credit: https://github.com/graphql-python/graphene-django/issues/971#issuecomment-633507631
Bug: https://github.com/graphql-python/graphene-django/issues/1291
"""
@classmethod
def is_type_of(cls, root: Any, info: graphene.ResolveInfo) -> bool:
# Special handling for the Relay `Node`-field, which lives at the root
# of the schema. Inside the `graphene_django` type resolution logic
# we have very little type information available, and therefore it'll
# often resolve to an incorrect type. For example, a query for `Book:<UUID>`
# would return a `LibraryBook`-object, because `graphene_django` simply
# looks at `LibraryBook._meta.model` and sees that it is a `Book`.
#
# Here we use the `id` variable from the query to figure out which type
# to return.
#
# See: https://github.com/graphql-python/graphene-django/issues/1291
# Check if the current path is evaluating a relay Node field
if info.path == ['node'] and info.field_asts:
# Support variable keys other than id. E.g., 'node(id: $userId)'
# Since `node(id: ...)` is a standard relay idiom we can depend on `id` being present
# and the value field's name being the key we need from info.variable_values.
argument_nodes = info.field_asts[0].arguments
if argument_nodes:
for arg in argument_nodes:
if arg.name.value == 'id':
# Catch direct ID lookups, e.g. 'node(id: "U3RvcmU6MQ==")'
if isinstance(arg.value, graphql.language.ast.StringValue):
global_id = arg.value.value
_type, _id = from_global_id(global_id)
return _type == cls.__name__
# Catch variable lookups, e.g. 'node(id: $projectId)'
variable_name = arg.value.name.value
if variable_name in info.variable_values:
global_id = info.variable_values[variable_name]
_type, _id = from_global_id(global_id)
return _type == cls.__name__
return super().is_type_of(root, info)
Graphene 3
via August 19th, 2024, adaptation of above:
class FixRelayNodeResolutionMixin:
"""
Fix issue where DjangoObjectType using same model aren't returned in node(id: )
Credit: https://github.com/graphql-python/graphene-django/issues/971#issuecomment-633507631
Bug: https://github.com/graphql-python/graphene-django/issues/1291
"""
@classmethod
def is_type_of(cls, root: Any, info: graphene.ResolveInfo) -> bool:
# Special handling for the Relay `Node`-field, which lives at the root
# of the schema. Inside the `graphene_django` type resolution logic
# we have very little type information available, and therefore it'll
# often resolve to an incorrect type. For example, a query for `Book:<UUID>`
# would return a `LibaryBook`-object, because `graphene_django` simply
# looks at `LibraryBook._meta.model` and sees that it is a `Book`.
#
# Here we use the `id` variable from the query to figure out which type
# to return.
#
# See: https://github.com/graphql-python/graphene-django/issues/1291
# Check if the current path is evaluating a relay Node field
if info.path.as_list() == ['node'] and info.field_nodes:
# Support variable keys other than id. E.g., 'node(id: $userId)'
# Since `node(id: ...)` is a standard relay idiom we can depend on `id` being present
# and the value field's name being the key we need from info.variable_values.
argument_nodes = info.field_nodes[0].arguments
if argument_nodes:
for arg in argument_nodes:
if arg.name.value == 'id':
# Catch direct ID lookups, e.g. 'node(id: "U3RvcmU6MQ==")'
if isinstance(arg.value, graphql.language.ast.StringValueNode):
global_id = arg.value.value
_type, _id = from_global_id(global_id)
return _type == cls.__name__
# Catch variable lookups, e.g. 'node(id: $projectId)'
variable_name = arg.value.name.value
if variable_name in info.variable_values:
global_id = info.variable_values[variable_name]
_type, _id = from_global_id(global_id)
return _type == cls.__name__
return super().is_type_of(root, info)