graphene-django
graphene-django copied to clipboard
`DjangoObjectType` using the same django model do not resolve to correct relay object
[!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 #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 #104 by @Tritlo.
-
Feb 6, 2017: Bug reported by @nickhudkins #107.
-
Feb 12, 2017: #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 #104 might be related :)
-
Feb 20, 2017: Replaced by #115 by @syrusakbary:
Merged to master https://github.com/graphql-python/graphene-django/commit/c635db5e5a83bb777c99514f06e3c906163eb57b.
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)
Thanks for the very detailed issue. The workaround worked for me except for a specific case:
Let's say there is a Project model which has many User.
class ProjectType(FixRelayNodeResolutionMixin, DjangoObjectType):
class Meta:
model = Project
In the above example the wrong UserType will be resolved even with the fix. The solution I found was to manually define and resolve UserType on ProjectType.
You can also create a separate django model that's a proxy of your Org model and use that as the model.
# models.py
class Org(models.Model):
...
class AnonymousOrg(Org):
class Meta:
proxy = True
# types.py
import graphene_django
from .models import Org as OrgModel, AnonymousOrg as AnonymousOrgModel
class Org(graphene_django.DjangoObjectType):
class Meta:
model = OrgModel
fields = (
"id",
"name",
"billing"
)
class AnonymousOrg(graphene_django.DjangoObjectType):
class Meta:
model = AnonymousOrgModel
fields = (
"id",
"name",
)
The only thing is there's a pitfall if there are many to many relationships and presumably many to one ie relationships that are set on the other model.