graphene-django icon indicating copy to clipboard operation
graphene-django copied to clipboard

`DjangoObjectType` using the same django model do not resolve to correct relay object

Open tony opened this issue 3 years ago • 2 comments

[!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)

tony avatar Jan 23 '22 22:01 tony

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.

Aebrathia avatar Feb 22 '22 08:02 Aebrathia

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.

TomsOverBaghdad avatar Dec 16 '22 22:12 TomsOverBaghdad