Search code examples
pythondjangographqlgraphene-pythongraphene-django

How to use a django abstract class with graphene-django?


I'm trying to have a unique interface for two concrete classes that are similar and inherit from a common abstract class.

My django model classes:

class Metadata(models.Model):
    name = models.CharField(max_length=255)
    sequence = models.PositiveSmallIntegerField()
    is_choices = False

    class Meta:
        abstract = True


class MetadataScalar(Metadata):
    string_format = models.CharField(max_length=255, blank=True, null=True)


class MetadataChoices(Metadata):
    is_choices = True
    choices = models.CharField(max_length=255, blank=True, null=True)

My graphene-django api:

class MetadataNode(DjangoObjectType):
    class Meta:
        interfaces = (Node,)
        connection_class = Connection
        model = Metadata
        fields = '__all__'


class MetadataScalarNode(MetadataNode):
    class Meta:
        interfaces = (Node,)
        connection_class = Connection
        model = MetadataScalar
        fields = '__all__'


class MetadataChoicesNode(MetadataNode):
    class Meta:
        interfaces = (Node,)
        connection_class = Connection
        model = MetadataChoices
        fields = '__all__'


class CreateMetadata(ClientIDMutation):
    metadata = Field(MetadataNode)

    class Input:
        name = String(max_length=255, required=True)
        sequence = Int(required=True)
        string_format = String()
        choices = List(String)

    @classmethod
    def mutate_and_get_payload(cls, root, info, **input):
        if 'string_format' in input:
            metadata = MetadataScalar.objects.create(
                name=input.get('name'),
                sequence=input.get('sequence'),
                string_format=input.get('string_format')
            )
        elif 'choices' in input:
            metadata = MetadataChoices.objects.create(
                name=input.get('name'),
                sequence=input.get('sequence'),
                choices=','.join(input.get('choices'))
            )
        return CreateMetadata(metadata=metadata)

When querying the graphql mutation corresponding to CreateMetadata, the metadata concrete class is successfully created. ;-)

The problem is that when the query asks for the created concrete Metadata in the result (here either MetadataScalar or MetadataChoices), graphql cannot find a node for the concrete class and outputs the following error message:

Expected value of type "MetadataNode" but got: MetadataScalar.

For your information, here is one example query:

mutation {
  createMetadata (input: {
    stringFormat: "foo"
    sequence: 12
    name: "bar"
  }) {
    metadata {
      name
      sequence
    }
  }
}

How to make it work nicely, without having to specify two different result types (metadataScalar and metadataChoices variables) in the second part of the query?


Solution

  • You can use Union in order to be able to specify multiple different result classes.

    In your case, that woud be:

    class MetadataScalarNode(DjangoObjectType):
        class Meta:
            interfaces = (Node,)
            connection_class = Connection
            model = MetadataScalar
            fields = '__all__'
    
    
    class MetadataChoicesNode(DjangoObjectType):
        class Meta:
            interfaces = (Node,)
            connection_class = Connection
            model = MetadataChoices
            fields = '__all__'
    
    
    class MetadataNode(Union):
        class Meta:
            types = (MetadataScalarNode, MetadataChoicesNode)
    

    The graphql query wil look like:

    mutation {
      createMetadata (input: {
        stringFormat: "foo"
        sequence: 12
        name: "bar"
      }) {
        metadata {
          __typename
          ... on MetadataScalarNode {
            name
            sequence
            stringFormat
          }
          ... on MetadataChoicesNode {
            name
            sequence
            choices
          }
        }
      }
    }