Trashinator - Graphql + Django Notes

GraphQL is turning out to be a nice alternative to REST, particulary when developing web-application functions that don't merit sharable links. There are plenty of tutorials, this is more a cheat-sheet for the Graphene library as it pertains to Django. If you're not a Django developer, you'll want to sit this one out.

The Graphene-Django library handles parsing graphql queries, it only requires that you describe the names of the queries and objects you want it to make available.

I'm using code from the Trashinator for examples.

Nodes describe discrete objects that may be queried; they tell Graphene how to identify and access your underlying object models.

# trashinator/schema.py describes the Django app's model and query structure to
# the graphene library

import graphene
from graphene_django import DjangoObjectType

# ...

class TrashNode(DjangoObjectType):

    # "Trash" is the underlying Djano model for this node.  Grahene
    # automatically understands the standard Django model field types, thanks
    # to the DjangoObjectType inheritance

    class Meta:
        model = Trash


    # 'litres' and 'gallons' are properties on the Trash model, not normal Django
    # fields.  It is possible to produce any of Graphene's Scalars on a node
    # such as this by adding the 'attribute' and a related 'resolve_attribute'
    # function.  Get the 'resolve_attribute' spelling right! It won't raise
    # errors, so troubleshooting a typo here is confusing.

    litres = graphene.Float()

    def resolve_litres(root, info):
        return root.litres

    gallons = graphene.Float()

    def resolve_gallons(root, info):
        return root.gallons

Queries provide Read access to your data.

class TrashQuery(graphene.ObjectType):

    # all_trash will produce a "query {allTrash ..." Graphql interface.  Note
    # the resolver below. "token" becomes an argument that the resolver can
    # accept from the end-user.  Marking the all_trash list as "required"
    # informs the library (and clients) that making this query will always
    # produce a list of results (even if the list is empty).

    all_trash = graphene.List(TrashNode, token=graphene.String(required=True),
                              required=True)

    # trash will produce a "query {Trash ..." Graphql interface, and similarly
    # requires a resolver.  This query produces either 0 or 1 results, so the
    # graphene.Field is *not* required from the resolver.  The date and token
    # arguments, however, are provided by the client and are required.

    trash = graphene.Field(
        TrashNode,
        date=graphene.types.datetime.Date(required=True),
        token=graphene.String(required=True))

    def resolve_all_trash(self, info, token, **kwargs):
        """Collect all the User's Trash"""
        user = utils.jwt_user(token)

        if not user.is_authenticated:
            raise ValueError("not authorized")

        return Trash.objects.filter(household__user=user)

    def resolve_trash(self, info, date, token, **kwargs):
        user = utils.jwt_user(token)

        if not user.is_authenticated:
            raise ValueError("not authorized")

        return Trash.objects.get(household__user=user, date=date)

Mutations can provide Create, Update, and Delete access to your data.

class SaveTrash(graphene.Mutation):

    # As with the Query, "trash" describes the data that will be returned to
    # the client.  It is required here to assure the client that this will
    # always be returned on a successful query.
    trash = graphene.Field(TrashNode, required=True)

    # Arguments on graphene.Mutations are handled in a class.  It is also
    # common to build this class separately and inherit it here.
    class Arguments:
        token = graphene.String(required=True)
        date = graphene.types.datetime.Date(required=True)
        metric = Metric()
        volume = graphene.Float()

    # The 'mutate' function arguments must be present in the Argument class.
    # The specific implementation should be dictated by the needs of your app.

    def mutate(self, info, token, date, metric=None, volume=None, **kwargs):
        user = utils.jwt_user(token)

        if not user.is_authenticated:
            raise ValueError("not authorized")

        try:
            trash = Trash.objects.get(household__user=user, date=date)
        except Trash.DoesNotExist:
            trash = Trash(
                household=user.trash_profile.current_household,
                date=date,
                litres=0)

        if volume is not None:
            if metric == Metric.Gallons:
                trash.gallons = volume
            elif metric == Metric.Litres:
                trash.litres = volume
            else:
                raise ValueError("metric must be litres or gallons")

            trash.save()

        return SaveTrash(trash=trash)


# Both the Read and Mutation queries are ultimately handled by a
# graphene.ObjectType, where the name of the query is based on the provided
# attribute.  In this case, save_trash will become "mutation {saveTrash ...".
# However, the mutation query is typically written as a separate class and
# added as a field to the final graphene.ObjectType.

class TrashMutation(graphene.ObjectType):
    save_trash = SaveTrash.Field()