graphql-python / graphene-django

Build powerful, efficient, and flexible GraphQL APIs with seamless Django integration.
http://docs.graphene-python.org/projects/django/en/latest/
MIT License
4.31k stars 769 forks source link

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

Open tony opened 2 years ago

tony commented 2 years ago

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

Environment

History

Other

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)
Aebrathia commented 2 years ago

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.

TomsOverBaghdad commented 1 year ago

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.