Django et GraphQL

nora.nckm.eu

Illustration

Mise en place d'une architecture avec un backend Django et un frontend Vue communiquant via une API GraphQL. Les sources sont disponibles sur sourcehut.

Partie serveur : Django

Il existe deux modules pour la prise en charge de GraphQL dans Django : graphene-django et strawberry-graphql-django. Il faut donc installer les modules graphene-django et django-cors-headers. Ce dernier permettant de gérer les requêtes cross-origin entre les parties serveur et client.

Note

Pour gérer les fonctionnalités avancées comme la pagination (graphql-relay) ou les filtres (django-filter), voir la documentation.

Configuration des modules dans settings.xml :

INSTALLED_APPS = [
    ...
    'graphene_django',
    'corsheaders',
    'app',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    ...
]

GRAPHENE = {
    'SCHEMA': 'app.schema.schema'
}

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = ("http://localhost:8080",)

Le middleware Cors est configuré pour autoriser les requêtes provenant de localhost:8080. Graphene est configuré pour récupérer le schéma GraphQL dans la variable app.schema.schema.

Définition des routes dans urls.py :

from graphene_django.views import GraphQLView

urlpatterns = [
    path('graphql/', GraphQLView.as_view(graphiql=True)),
]

Le point d'entrée de l'API GraphQL est défini ici. Un éditeur de requêtes est accessible à l'adresse http://localhost:8000/graphql/.

Création du schéma GraphQL dans app/schema.py :

import graphene
from django.contrib.auth import get_user_model
from graphene_django import DjangoObjectType

from app import models


# Définition des types de données basés sur les modèles Django
class UserType(DjangoObjectType):
    class Meta:
        model = get_user_model()


class AuthorType(DjangoObjectType):
    class Meta:
        model = models.Profile


class PostType(DjangoObjectType):
    class Meta:
        model = models.Post
        #fields = ("title", "subtitle")


class TagType(DjangoObjectType):
    class Meta:
        model = models.Tag


# Définition des requêtes de sélection (query)
class Query(graphene.ObjectType):
    all_posts = graphene.List(PostType)
    posts_by_author = graphene.List(PostType, email=graphene.String())
    posts_by_tag = graphene.List(PostType, tag=graphene.String())
    post_by_slug = graphene.Field(PostType, slug=graphene.String())
    author_by_email = graphene.Field(AuthorType, email=graphene.String())

    def resolve_all_posts(self, info):
        return models.Post.objects.prefetch_related("tags").select_related("author").all()

    def resolve_posts_by_author(self, info, email):
        return models.Post.objects.prefetch_related("tags").select_related("author").filter(author__user__email=email)

    def resolve_posts_by_tag(self, info, tag):
        return models.Post.objects.prefetch_related("tags").select_related("author").filter(tags__name__iexact=tag)

    def resolve_post_by_slug(self, info, slug):
        return models.Post.objects.prefetch_related("tags").select_related("author").get(slug=slug)

    def resolve_author_by_email(self, info, email):
        if not info.context.user.is_authenticated():
            models.Profile.objects.none()
        return models.Profile.objects.select_related("user").get(user__email=email)


# Définition des requêtes de modification (mutation)
class PostMutation(graphene.Mutation):
    class Arguments:
        subtitle = graphene.String(required=True)
        id = graphene.ID()

    post = graphene.Field(PostType)

    @classmethod
    def mutate(cls, root, info, subtitle, id):
        post = models.Post.objects.get(pk=id)
        post.subtitle = subtitle
        post.save()
        return PostMutation(post=post)


class Mutation(graphene.ObjectType):
    update_post = PostMutation.Field()


# Définition du schéma GraphQL à partir des queries et des mutations
schema = graphene.Schema(query=Query, mutation=Mutation)

Une mutation peut aussi se baser sur un formulaire Django : voir les classes DjangoFormMutation et DjangoModelFormMutation dans la documentation.

Exemple de requête GraphQL :

query {
    allPosts {
        id
        title
        subtitle
        author {
            user {
                email
            }
        }
        tags {
            name
        }
    }
}

Exemple de mutation GraphQL :

mutation {
    updatePost(id: 1, subtitle: "new subtitle") {
        post {
            id
            title
            subtitle
        }
    }
}

Partie client : Vue

Du côté client on peut mettre en place une application Vue.js. Le plugin Vue Apollo permet de gérer les requêtes GraphQL dans un projet Vue.

Les fragments constituent des morceaux de requêtes GraphQL et sont définis dans un fichier fragments.js :

import gql from 'graphql-tag'

export const authorFragment = {
    author: gql`
        fragment AuthorFragment on AuthorType {
            author {
                user {
                    email
                    firstName
                    lastName
                }
            }
        }
    `,
}

export const postFragment = {
    post: gql`
        fragment PostFragment on PostType {
            title
            subtitle
            publishDate
            published
            metaDescription
            slug
            tags {
                name
            }
        }
    `,
}

Dans les composants Vue, les requêtes ressemblent à ceci :

import gql from 'graphql-tag'

const posts = await this.$apollo.query({
    query: gql`
        query {
            allPosts {
                ...PostFragment
                ...AuthorFragment
            }
        }
        ${postFragment}
        ${authorFragment}
    `,
})
this.allPosts = posts.data.allPosts

const posts = await this.$apollo.query({
    query: gql`
        query ($tag: String!) {
            postsByTag(tag: $tag) {
                ...PostFragment
                ...AuthorFragment
            }
        }
        ${postFragment}
        ${authorFragment}
    `,
    variables: {
        tag: this.$route.params.tag,
    },
})
this.posts = posts.data.postsByTag

const post = await this.$apollo.query({
    query: gql`
        query ($slug: String!) {
            postBySlug(slug: $slug) {
                ...PostFragment
                ...AuthorFragment
            }
        }
        ${postFragment}
        ${authorFragment}
    `,
    variables: {
        slug: this.$route.params.slug,
    },
})
this.post = post.data.postBySlug

const user = await this.$apollo.query({
    query: gql`
        query ($email: String!) {
            authorByEmail(email: $email) {
                website
                bio
                user {
                    firstName
                    lastName
                    email
                }
                postSet {
                    ...PostFragment
                }
            }
        }
        ${postFragment}
    `,
    variables: {
        email: this.$route.params.email,
    },
})
this.author = user.data.authorByEmail

Dans les composants Vue, les mutations ressemblent à ceci :

import gql from 'graphql-tag'

this.$apollo.mutate({
    mutation: gql`
        mutation ($id: ID!, $subtitle: String!) {
            updatePost(id: $id, subtitle: $subtitle) {
                ...PostFragment
                ...AuthorFragment
            }
        }
        ${postFragment}
        ${authorFragment}
    `,
    variables: {
        id: this.post.id,
        subtitle: this.post.subtitle,
    }
})

Un commentaire sur un de mes articles ? Commencez une discussion sur ma liste de diffusion en envoyant un email à ~nora/public-inbox@lists.sr.ht [règles]