graphql-python / graphql-python/graphene-django
`DjangoObjectType` using the same django model do not resolve to correct relay object
- Dominant language
- Python
- Stars
- 4.4k
- Forks
- 760
- PR merge metrics
- No merged PRs in 30d
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:
```python
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`.
```js
btoa('Org:7eca71ed-ff04-4473-9fd1-0a587705f885')
'T3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ=='
```
```graphql
{
node(id: "T3JnOjdlY2E3MWVkLWZmMDQtNDQ3My05ZmQxLTBhNTg3NzA1Zjg4NQ==") {
id
__typename
... on Org {
id
}
}
}
```
Response (incorrect):
```js
{
"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):
```js
{
"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](https://github.com/graphql-python/graphene-django/issues/107#issuecomment-279243056) by @syrusakbary:
> Right now you can make this work with using a new registry for the second definition.
>
> ```python
> 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](https://github.com/graphql-python/graphene-django/commits/main/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.
```js
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](https://github.com/graphql-python/graphene-django/issues/971#issuecomment-633507631) on May 25, 2020:
```python
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
```python
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:`
# 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:
```python
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:`
# 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)
```
Contributor guide
Assessment
This issue has not been assessed yet.