Package: graph-wrap
GraphWrapGraphWrap is a python library which, by adding only two lines of code to your django project, can extend an existingDjango Rest Framework(orTastypie)
API with aGraphQLinterface.
This is achieved by leveragingGraphene-Djangoto dynamically
build, at runtime, a GraphQL ObjectType for each Django REST (or tastypie) view in your API. These ObjectTypes are then glued together to
form a GraphQL schema which has the same "shape" as your existing REST API.
Note that GraphWrap isnotdesigned to build a GraphQL schema to replace your existing REST API,
but rather extend it to offer an additionalfully compliantGraphQL-queryable interface.Highlights:The dynamic nature of the build of the GraphQL layer means that you can continue to develop your existing
REST based API and know that the GraphQL schema will be kept up-to-date automatically.Since the GraphQL layer is using the REST API under-the-hood, you can be sure that important things
likeserialization,authentication,authorizationandfilteringwill be consistent between your REST view
and the corresponding GraphQL type.You no longer need to "over expose" fields from nested apis - the client can make use of the GraphQL layer
to fetch data they need. This can lead to significant performance boosts
in certain circumstances (One of the advantages of GraphQL queries is that they solve then+1 problemwhich occurs with traditional REST-based APIs).Which problems does GraphWrap address?A common pattern for circumventing then+1 problemon a REST API is to expose
fields from "nested" serializers on a parent serializer. For example, here we expose
fields from theAuthorSerializeron thePostViewSet:classAuthorSerializer(serializers.ModelSerializer):classMeta:model=Authorfields=['name','active']classPostSerializer(serializers.ModelSerializer):author=AuthorSerializer(source='author')classMeta:model=Postfields=['author','content']classPostViewSet(viewsets.ReadOnlyModelViewSet):queryset=Post.objects.all()serializer_class=PostSerializerWhilst this solvesn+1 problem, it creates a whole new
class of problem. The issue now is that we're potentiallyover exposingthe nested author fields:
we may have one api client who is interested in these nested fields, but we may also have several
for whom these fields are irrelevant and who do not appreciate the extra time it now takes to fetch
and serialize this additional data. Unless we start building an APIper client(which of course
we do not want), we're a bit stuck.Enter GraphQL: GraphQL is designed so that the client decides what info it receives from the server,
not the other way around. Whilst many greatpackagesexist to create a GraphQL API from scratch, migrating an mature production REST API
to use one of these frameworks is not so simple. It may also be that our REST API
has functionality which is not available on a GraphQL specific API.This is where GrapWrap comes in: by adding two lines of code to your project, GraphWrap
exposes a GraphQL schema which has the same "shape" as your existing REST API.
With this new endpoint, we can now stop overexposing theauthorfields and instead
simply exposeauthoras a URL:classPostSerializer(serializers.ModelSerializer):author=serializers.HyperlinkedRelatedField(view_name='author-detail',read_only=True)This keeps our clients who don't care about the nested author fields happy.
Any client interested in retrieving the nested author fields can then do so via a query to the new/graphqlendpoint:query{all_posts{contentauthor{nameactive}}}The important point here is that the above query will authentication, permissions and
serialization coming from the corresponding Django REST Post and Author viewsets/serializers.LimitationsHere are a few limitations of the GraphQL API produced by GraphWrap:It can only accept GraphQLqueries- mutations and subscriptions
are not (yet) supported.The schema is built only from Django REST Framework views which inherit fromModelViewSet(orReadOnlyModelViewSet) and which are registered via a router which inherits fromSimpleRouter. Alternatively, if you're
using tastypie, the schema is only built from resources inheriting fromModelResource.Will only work for APIs which use JSON serialization.GraphWrap for the Django REST FrameworkQuick startPrerequisitesBefore using this library, you must be using Python 3.6 (or later) and have the following installed:Django >=2.2djangorestframework>=3.0.0Installingpipinstallgraph_wrapExposing the /graphql endpointGraphWrap exposes the GraphQL schema via a Django viewgraphql_view. This view builds and exposes a GraphQL
queryable schema via a POST request to a/graphqlendpoint. The code snippet below demonstrates by example
how you can transform your DRF REST-API into a GraphQL schema by adding just two lines of code to your project:fromrest_frameworkimportroutersfromgraph_wrap.django_rest_framework.graphql_viewimportgraphql_view# Addition 1: import the graphql_viewfromtests.django_rest_framework_api.apiimport(AuthorViewSet,PostViewSet)router=routers.SimpleRouter()router.register(r'author',AuthorViewSet)router.register(r'post',PostViewSet)urlpatterns=[path(r'',include(api.urls)),path(r'/graphql/',view=graphql_view),# Addition 2: Register the view under the URL /graphql.]Documentation (by Example)In this section we give a brief overview of how to use GraphWrap with Django REST Framework via examining
a simple concrete example.Set-upSuppose we have the following basic django models and corresponding DRF API (
a fully executable (but more complex) version of this example can be found in graph_wrap.tests):# models.pyclassMedia(models.Model):name=models.TextField()content_type=models.TextField(null=True)size=models.BigIntegerField(null=True)classAuthor(models.Model):user=models.ForeignKey(User,on_delete=models.PROTECT,null=True)name=models.TextField()age=models.IntegerField(null=True)active=models.BooleanField(default=True)profile_picture=models.ForeignKey(Media,null=True,on_delete=models.PROTECT)defget_name(self):# Use to test custom additional serializationreturnself.name.upper()classPost(models.Model):content=models.TextField()date=models.DateTimeField()author=models.ForeignKey(Author,null=True,on_delete=models.SET_NULL,related_name='entries')files=models.ManyToManyField('Media')rating=models.DecimalField(null=True,decimal_places=20,max_digits=40)# api.pyclassUserSerializer(serializers.ModelSerializer):classMeta:model=Userfields=['username','is_staff']classAuthorSerializer(serializers.ModelSerializer):user=UserSerializer()entries=serializers.PrimaryKeyRelatedField(many=True,queryset=Post.objects.all())classMeta:model=Authorfields=['name','age','rating','profile_picture','user','entries']classPostSerializer(serializers.ModelSerializer):written_by=serializers.HyperlinkedRelatedField(view_name='author-detail',read_only=True)classMeta:model=Postdepth=3fields=['written_by','content','date','files']classAuthorViewSet(viewsets.ModelViewSet):queryset=Author.objects.all()serializer_class=AuthorSerializerclassPostViewSet(viewsets.ReadOnlyModelViewSet):queryset=Post.objects.all()serializer_class=PostSerializerfilter_backends=[filters.SearchFilter,DjangoFilterBackend]search_fields=['content','author__name']filterset_fields=['author__name','content']If we wish to layer our REST resources with a GraphQL interface, we can follow the instructions above in the
"Quickstart" guide where we import thegraphql_viewand expose it via the/graphqlurl:fromgraph_wrap.django_rest_framework.graphql_viewimportgraphql_view# Addition 1: import the graphql_viewfromtests.django_rest_framework_api.apiimport(AuthorViewSet,PostViewSet)router=routers.SimpleRouter()router.register(r'author',AuthorViewSet)router.register(r'post',PostViewSet)urlpatterns=[path(r'',include(api.urls)),path(r'/graphql/',view=graphql_view),# Addition 2: Register the view under the URL /graphql.]Understanding the SchemaWith these simple changes, we can now query the/graphqlendpoint with GraphQL queries. The structure
queries can take, as with all GraphQL APIs, is dictated by the shape of the underlyingschema(which, in this case, is dictated by the shape of the Django REST Framework API). To see what the schema looks like, run the following:>>>fromgraph_wrap.django_rest_frameworkimportschema
>>>print(schema())schema{query:Query}typeQuery{author(id:Int!):author_typeall_authors:[author_type]post(id:Int!):post_typeall_posts(search:String,orm_filters:String):[post_type]}typeauthor_type{name:String!age:Intactive:Boolean!profile_picture:Stringuser:user_type!entries:[String]!}typepost_type{written_by:author_type!content:String!date:String!files:[post__files_type]!rating:String}typepost__files_type{id:Int!name:String!content_type:Stringsize:Int}typeuser_type{username:String!is_staff:Boolean!}Important points to note about the schema produced by GraphWrap:snake_case: As can be seen above, GraphWrap produces a schema in thesnake_caseconvention. Whilst
this is generally not favoured in GraphQL circles, it was chosen here as it would likely be more consistent with
the field names on the underlying REST resources (which would use most often use the PEP8 recommended snake
case convention).Root Query fields: For each DRF ModelViewSet in our API, GraphWrap adds to the Query type precisely
two fields - one corresponding to the data accessible via a GET request to the 'list' endpoint of the
resource, and one corresponding to the data accessible via a GET request to the 'detail' endpoint of the
resource. If we take our AuthorViewSet as an example:the 'list' endpoint corresponds to the url/author. This maps to theall_authorsfield on the Query type.
Note that it is possible to change the naming convention of the list endpoint field by setting theLIST_ENDPOINT_RESOLVER_PREFIXvalue insettings.py. For example, settingLIST_ENDPOINT_RESOLVER_PREFIX = ''will mean this list endpoint url corresponds to the nameauthors.the 'detail' endpoint corresponds to urls of the form '/author/{author_pk}'. This maps to theauthor(id: Int!)field on the Query type (where, in the usual GraphQL schema syntax,(id: Int!)indicates that an integer author
id must be supplied.)ObjectType and ObjectType Field names:GraphWrap maps each modelserializerin our DRF API to a GraphQL
ObjectType (including dynamically buildnested serializers.The naming convention of the resultant ObjectType depends from which serializer it was created:If the ObjectType corresponds to an explicit "non-nested" serializer, the name of the field can be
found by appending_typeto the lowercase version name of the underlying serializer model. For example,
theAuthorSerializercorresponds to theauthor_typein the above.If the ObjectType comes from a dynamically createdNestedSerializer, the name of the field follows the Django
query notation:{parent_model}__{related_field}_type. For example, in our API above thePostSerializeris set to havedepth=3. This creates aNestedSerializerfor thefilesfield, which corresponds to thepost__files_typein the above.Filtering (orm_filtersandsearch):Currently, the/graphqlendpoint produced by GraphWrap supports two types of filtering used by the Django REST
Framework:Generic Filteringvia the
django_filtersDjangoFilterBackendandSearchFilter.When a DRF ViewSet allows filtering via theDjangoFilterBackend, the correspondingQueryfield on the
GraphQL schema produced by GraphWrap will have an optionalorm_filtersargument. If we take ourPostViewSetas an example, then the filtering done by the REST GET query/paul/?author__name=Paulcan be achieved
via a POST request to/graphqlwith the following query:{all_posts(orm_filters:"author__name=Paul"){content# or any fields belonging to post_type}}Similarly, when a DRF ViewSet allows filtering via theSearchFilter, the correspondingQueryfield on the
GraphQL schema produced by GraphWrap will have an optionalsearchargument. This can be used in a similar
fashion.Authentication and Authorization of /graphql endpointThe authentication/authorization applied
when querying/graphqlis the authentication/authorization defined on the resource corresponding to the root field
of the query applied. This is consistent with the way DRF handles authenticaiton/authorization.
So, for example, the following query would invoke whatever authentication/authorization
was defined on theAuthorViewSet:{all_authors{name}}Making Queries: REST vs GraphQLIn this section we'll look at how various REST GET requests can be mapped to queries for the/graphqlendpoint. Again, we'll do this via examining our explicit concrete example (note that the queries
and requests pictured in this section were produced on theInsomniaHTTP client, which has a integration with GraphQL). Some of the fields here might not match up exactly
with our example above, but hopefully the idea is clear:'list' endpoint requestsREST:GraphQL'detail' endpoint requestsRESTGraphQLFilteringRESTGraphQLSome fancier GraphQL query examples - see GraphQLqueriesfor moreNestingFragmentsGraphWrap for TastypieQuick startCore RequirementsBefore using this library, the following requirements must be met:Your project is usingPython >= 3.6andDjango >=2.2.Installingpipinstallgraph_wrapRegistering the GraphQL endpointGraphWrap exposes the GraphQL schema via a Django viewgraphql_view. This view builds and exposes a GraphQL
queryable schema via a POST request to a/graphqlendpoint. The code snippet below demonstrates by example
how you can transform your Tastypie into a GraphQL schema by adding just three lines of code to your project:# tests.urls.pyfromgraph_wrap.tastypie.graphql_viewimportgraphql_view# add this line to your projecturlpatterns=[...,path(r'/graphql/',view=graphql_view),# Register the view under the URL /graphql.]In order for GraphQL to be able to build the GraphQL schema from the tastypie Api instance, it needs
to know where that instance lives in your project. To allow GraphWrap to locate the Api instance, we can simply
add the full path of the instance to our django settings module. For example:# tests.settings.pyTASTYPIE_API_PATH='tests.urls.api'Documentation (by Example)In this section we give a brief overview of how to use GraphWrap via examining
a simple concrete example.Set-upSuppose we have the following basic django models and corresponding tastypie resources (
a fully executable version of this example can be found in graph_wrap.tests):# models.pyclassAuthor(models.Model):name=models.TextField()age=models.TextField()classPost(models.Model):content=models.TextField()date=models.DateTimeField()author=models.ForeignKey(Author,null=True,on_delete=models.SET_NULL)files=models.ManyToManyField('Media')classMedia(models.Model):name=models.TextField()content_type=models.TextField()size=models.BigIntegerField()# api.pyclassAuthorResource(ModelResource):posts=fields.ManyToManyField('tests.api.PostResource',attribute='post_set')classMeta:queryset=Author.objects.all()resource_name='author'filtering={'age':('exact',),'name':('exact',),}classPostResource(ModelResource):author=fields.ForeignKey(AuthorResource,attribute='author',null=True)files=fields.ManyToManyField('tests.api.MediaResource',attribute='files')date=fields.DateTimeField('date')classMeta:queryset=Post.objects.all()resource_name='post'classMediaResource(ModelResource):classMeta:queryset=Media.objects.all()resource_name='media'If we wish to layer our REST resources with a GraphQL interface, we can follow the instructions above in the
"Quickstart" guide. Start by registering our GraphQLResource with the tastypie Api instance:# urls.pyfromdjango.contribimportadminfromdjango.urlsimportpath,includefromtastypie.apiimportApifromgraph_wrapimportGraphQLResourcefromtests.apiimportAuthorResource,PostResource,MediaResourceapi=Api('v1')api.register(AuthorResource())api.register(PostResource())api.register(MediaResource())api.register(GraphQLResource())urlpatterns=[path(r'',include(api.urls)),path('admin/',admin.site.urls),]Next, add theTASTYPIE_API_PATHto the django settings module so GraphWrap can locate the tastypie Api:TASTYPIE_API_PATH='tests.urls.api'Understanding the SchemaWith these simple changes, we can now query the/graphqlendpoint with GraphQL queries. The structure
queries can take, as with all GraphQL APIs, is dictated by the shape of the underlying schema (which, in this case, is
dictated by the shape of the tastypie API). To see what the schema looks like, run the following:>>>fromgraph_wrapimportschema
>>>schema=schema()>>>print(schema)schema{query:Query}typeQuery{author(id:Int!):author_typeall_authors(orm_filters:String):[author_type]post(id:Int!):post_typeall_posts(orm_filters:String):[post_type]media(id:Int!):media_typeall_medias(orm_filters:String):[media_type]}typeauthor_type{resource_uri:String!posts:[post_type]!id:Int!name:String!age:String!}typemedia_type{resource_uri:String!id:Int!name:String!content_type:String!size:Int!}typepost_type{resource_uri:String!author:author_typefiles:[media_type]!date:String!id:Int!content:String!}Important points to note about the schema produced by GraphWrap:snake_case: As can be seen above, GraphWrap produces a schema in thesnake_caseconvention. Whilst
this is generally not favoured in GraphQL circles, it was chosen here as it would likely be more consistent with
the field names on the underlying REST resources (which would use most often use the PEP8 recommended snake
case convention).Root Query field names: For each REST model-resource, GraphWrap adds to the Query type precisely
two fields - one corresponding to the data accessible via a GET request to the 'list' endpoint of the
resource, and one corresponding to the data accessible via a GET request to the 'detail' endpoint of the
resource. If we take our AuthorResource as an example:the 'list' endpoint corresponds to the url/author. This maps to theall_authorsfield on the Query type.the 'detail' endpoint corresponds to urls of the form '/author/{author_pk}'. This maps to theauthor(id: Int!)field on the Query type (where, in the usual GraphQL schema syntax,(id: Int!)indicates that an integer author
id must be supplied.)ObjectType and ObjectType Field names:GraphWrap maps each model-resource maps to a GraphQL
ObjectType. The name of the resultant ObjectType can be found by appending_typeto the name of the
corresponding resource. For example, theAuthorResource, which has nameauthor, maps to theauthor_typeGraphQL ObjectType.The names of the fields on each ObjectType match those of the names of the fields on the corresponding
resource.Filtering (orm_filters): Notice in the schema above that eachall_field can be queried with an optionalorm_filtersargument. This is the GraphQL equivalent of the ORM filtering offered by tastypie on list endpoints.
If we take our AuthorResource as an example (which has been defined withfiltering = {'age': ('exact',), 'name': ('exact',)}), then the REST GET query/author/?name=Paulcan be achieved
via a POST request to/graphqlwith the following query:{all_authors(orm_filters:"name=Paul"){name}}Authentication and Authorization of GraphQLResourceThe authentication/authorization applied
when querying/graphqlis the authentication/authorization defined on the resource corresponding to the root field
of the query applied. This is consistent with the way tastypie handles authenticaiton/authorization.
So, for example, the following query would invoke whatever authentication/authorization
was defined on theAuthorResource:{all_authors(orm_filters:"name=Paul"){name}}