Search code examples
djangographqlgraphene-django

How to build GraphQL schema for reverse lookup from Content Type model to usual model?


I have 4 types of profiles. Some fields are the same, some are different. Each profile has its own url, so I use ContentType as a central place of mappring urls<->profiles.

# profiles/models.py
class Sport(models.Model):
    pass

class ProfileAbstract(models.Model):
    pass

class ProfileOrganization(ProfileAbstract):
    pass

class ProfilePlace(ProfileAbstract):
    pass

class ProfileTeam(ProfileAbstract):
    pass

class ProfileUser(ProfileAbstract):
    pass

class ProfileURL(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    slug = models.SlugField(max_length=30)  # Url

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.CharField(max_length=36)  # CharField because I use UUID
    content_object = GenericForeignKey('content_type', 'object_id')

P.S. Example of direct lookup can be found in this github issue.


Solution

  • # profiles/schema.py
    import graphene
    
    from graphene_django.types import DjangoObjectType
    
    from .models import (ProfileOrganization, ProfilePlace, ProfileTeam, ProfileUser,ProfileURL, Sport)
    
    # [ START: Types ]
    class SportType(DjangoObjectType):
        class Meta:
            model = Sport
    
    
    class ProfileURLType(DjangoObjectType):
        class Meta:
            model = ProfileURL
    
        slug = graphene.String()
        model_name = graphene.String()
    
        def resolve_slug(self, info):
            return self.slug
    
        def resolve_model_name(self, info):
            return self.content_object.__class__.__name__  # ex. 'ProfileTeam'
    
    
    class ProfileOrganizationType(DjangoObjectType):
        """
        Use this class as a basic class for other Profile Types classes
        """
    
        class Meta:
            model = ProfileOrganization
            fields = ('name', 'logo_data', 'profile_url')
    
        profile_url = graphene.Field(ProfileURLType)
    
        def resolve_profile_url(self, args):
            return self.profile_url.first()
    
    
    class ProfilePlaceType(ProfileOrganizationType):
        class Meta:
            model = ProfilePlace
    
    
    class ProfileTeamType(ProfileOrganizationType):
        class Meta:
            model = ProfileTeam
    
    
    class ProfileUserType(ProfileOrganizationType):
        class Meta:
            model = ProfileUser
    
    
    class ProfileTypeUnion(graphene.Union):
        class Meta:
            types = (ProfileOrganizationType, ProfileTeamType, ProfilePlaceType, ProfileUserType)
    # [ END: Types ]
    
    
    # [ START: Queries ]
    class Query(graphene.ObjectType):
        """
        EXAMPLE OF QUERY:
    
        query profileDetails {
          profileDetails(profileUrl: "8auB-pMH-6Sh") {
            ... on ProfileTeamType {
              id,
              name,
              skillLevel,
              sport {
                name
              },
              profileUrl {
                slug,
                modelName
              }
            }
    
            ... on ProfilePlaceType {
              id,
              name,
              sports {
                name
              },
              profileUrl {
                slug,
                modelName
              }
            }
          }
        }
        """
    
        profile_details = graphene.Field(ProfileTypeUnion, required=True, profile_url=graphene.String())
    
        def resolve_profile_details(self, info, profile_url):
            profile_url_type = ContentType.objects.get(app_label='profiles', model='profileurl')
            profile_url_inst = profile_url_type.get_object_for_this_type(slug=profile_url)
    
            return profile_url_inst.content_object
    # [ END: Queries ]
    

    ... on ProfileTeamType is inline fragments (details). As you can see, we query 2 fragments in the example above (however in my case it should be 4, according to the number of profiles type), but just one fragment/model returns data - the one which refers to the provided profileUrl in the query.


    From front end side based on the received modelName we can handle fields according to our needs.

    Also the query mentioned above with fragments > 1 throws 2 errors in browser's console (I use Apollo client on Front End):

    1. You are using the simple (heuristic) fragment matcher, but your queries contain union or interface types. Apollo Client will not be able to accurately map fragments. To make this error go away, use the IntrospectionFragmentMatcher as described in the docs: https://www.apollographql.com/docs/react/advanced/fragments.html#fragment-matcher

    (error has incorrect link. The correct one is this.)

    1. WARNING: heuristic fragment matching going on!.

    It seems this is a bug (github issue).

    In order to fix it, in Apollo settings we need to add cache option, which should look like:

    import ApolloClient from 'apollo-client';
    import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
    import { HttpLink } from 'apollo-link-http';
    
    const fragmentMatcher = new IntrospectionFragmentMatcher({
      introspectionQueryResultData: {
        __schema: {
          types: []
        }
      }
    })
    
    const cache = new InMemoryCache({ fragmentMatcher });
    
    const client = new ApolloClient({
      cache,
      link: new HttpLink(),
    });