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.3k stars 769 forks source link

Feature Request: DjangoInterface #1366

Open Smona opened 1 year ago

Smona commented 1 year ago

Is your feature request related to a problem? Please describe.

One of the main benefits of graphene_django is the de-duplication in field types and descriptions for model fields. However, it's common to have polymorphic models, which are more well suited to GraphQL interfaces. For example, the shape of a JSON field could vary depending on a type field.

There doesn't seem to be a good way to import model field info into interfaces like there is for ObjectTypes at the moment.

Describe the solution you'd like

It would be great if there was a DjangoInterface class which could be used for defining interfaces similarly to DjangoObjectType. This would allow for the following:

class Person(DjangoInterface):
    class Meta:
        model = PersonModel
        fields = ("id", "name", "birthday")

    @classmethod
    def resolve_type(cls, instance, info):
        return Employee if instance.employer else BasePerson

class BasePerson(DjangoObjectType):
    class Meta:
        model = PersonModel
        interfaces = (Person,)

class Employee(DjangoObjectType):
    class Meta:
        model = PersonModel
        fields = ("job_title")
        interfaces = (Person,)

    company = NonNull(String)

    def resolve_company(root, info):
        return root.employer.name

In this case, Employee would have the fields id, name, birthday, job_title, and company. BasePerson would have only id, name, and birthday.

Describe alternatives you've considered

Another option would be to tie all implementations of an interface to the same Django model. But using the model only to generate the field definitions and keeping the DjangoObjectType interface the same seems both simpler and more flexible.

Right now, the closest approach seems to be something like:

class MyInterface(graphene.Interface):
    my_field = convert_django_field(MyModel.my_field.field)

This works okay but is pretty hacky, relying on private interfaces and not working for relation fields.

erikwrede commented 1 year ago

You might find some inspiration for that here: https://github.com/graphql-python/graphene-sqlalchemy/pull/365

advl commented 8 months ago

I have been working on an implementation of poylmorphic models using graphene_django in the context of the ActivityStreams spec.

Based on this discussion I have created a DjangoInterface that mirror the DjangoObjectType definitions and general Meta api.

The only change I have found can be necessary is the removal of the "interfaces" meta prop, because of this - in other words interface extension doesn't seem widely supported and/or is not documented a lot.

There is additional change needed in the DjangoConnectionField, where we need to update an assertion to assert issubclass(_type, (DjangoObjectType, DjangoInterface)), (

In usage, it is important that a registry is used, such as

class PolymorphicRegistry(Registry):  
    """                             
    PolymorphicRegistry extends the functionality of the base Registry class  
    to support polymorphic model resolution. This is used to return different  
    GraphQL types based on the Django model's type, especially useful for  
    models that have inheritance.   
    """                             

    def get_type_for_model(self, model):
        """                         
        Retrieve the GraphQL type associated with the provided Django model.  
        If the model is a subclass of Object or is Object itself, a specific  
        interface type is returned. Otherwise, the default type resolution  
        mechanism is used.          

        :param model: The Django model class to resolve.                         
        :return: The GraphQL type associated with the model.
        """                       
        if issubclass(model, Object):
            return ObjectInterface
        return super().get_type_for_model(model)

... where Object is the base polymorphic model to be mapped to the Interface.

This approach works even if more testing is necessary.

One limitation is that Interface composition doesn't seem very easy, and that there are a few glitches when using the relay Node interface as well I have not taken a deep look at.

At the moment, since this is not part of the graphene_django library, I also needed to overwrite the single_dispatch converter function for relation fields, so that a custom connection field can be used to solve the assertion fail in the default DjangoConnectionField.

I would love to hear your thoughts on this approach and the viability of a PR that I will be happy to submit for consideration.