strawberry-django-plus

build status
coverage
PyPI version
python version
django version

Enhanced Strawberry integration with Django.

Built on top of strawberry-django
integration, enhancing its overall functionality.

Features

  • All of supported features by strawberry-django.
  • Extension that automatically optimize queries (using
    only/select_related/prefetch_related) to solve graphql N+1 problems, with support for
    fragment spread, inline fragments, @include/@skip directives, prefetch merging, etc
  • Improved sync/async resolver that priorizes the model’s cache to avoid have to use
    sync_to_async
    when not needed.
  • Support for Django choices enums using (requires
    django-choices-field)
  • Relay support for queries, connections and input mutations.
  • Django Debug Toolbar integration with graphiql to
    display metrics like SQL queries
  • (Coming Soon…) Improved Django mutations with automatic validation errors integration.
  • (Coming Soon…)Integration with Django Guardian for per-object permission management.
  • A well typed and documented API.

Installation

Install it with pip:

pip install strawberry-django-plus

Introduction

Since this lib has a long name, it does provide a shortcut called gql where all of
strawberry’s API and ours can be accessed.

from strawberry_django_plus import gql

# All strawberry's base api can be found directly on gql, like:
gql.type  # same as strawberry.type
gql.field  # same as strawberry.field
...

# The strawberry-django API and our custom implementation can be found on gql.django, like:
gql.django.type
gql.django.field
...

# We also have a custom relay implementation in here:
gql.relay

How To

Automatic QuerySet Optimization

The automatic optimization is enabled by adding the DjangoOptimizerExtension to your
strawberry’s schema config.

import strawberry
from strawberry_django_plus.optimizer import DjangoOptimizerExtension

schema = strawberry.Schema(
    Query,
    extensions=[
        DjangoOptimizerExtension(),
    ]
)

Now consider the following:

# models.py

class Artist(models.Model):
    name = models.CharField()

class Album(models.Moodel):
    name = models.CharField()
    release_date = models.DateTimeField()
    artist = models.ForeignKey("Artist", related_name="albuns")

class Song(models.Model):
    name = model.CharField()
    duration = models.DecimalField()
    album = models.ForeignKey("Album", related_name="songs")

# schema.py
from strawberry_django_plus import gql

@gql.django.type(Artist)
class ArtistType:
    name: auto
    albums: "List[AlbumType]"

@gql.django.type(Album)
class AlbumType:
    name: auto
    release_date: auto
    artist: ArtistType
    songs: "List[SongType]"

@gql.django.type(Song)
class SongType:
    name: auto
    duration: auto
    album_type: AlbumType

@gql.type
class Query:
    artist: Artist = gql.django.field()
    songs: List[SongType] = gql.django.field()

Querying the artist field:

{
  artist {
    id
    name
    albums {
      id
      name
      songs {
        id
        name
      }
    }
  }
}

# This will generate a query like:
Artist.objects.all().only("id", "name").prefetch_related(
    Prefetch(
        "albums",
        queryset=Album.objects.all().only("id", "name").prefetch_related(
            "songs",
            Song.objects.all().only("id", "name"),
        )
    ),
)

And querying the songs list:

{
  song {
    id
    album
    id
    name
    artist {
      id
      name
      albums {
        id
        name
        release_date
      }
    }
  }
}

# This will generate a query like:
Song.objects.all().only(
    "id",
    "album",
    "album__id",
    "album__name",
    "album__release_date",  # Note about this below
    "album__artist",
    "album__artist__id",
).select_related(
    "album",
    "album__artist",
).prefetch_related(
    "album__artist__albums",
    Prefetch(
        "albums",
        Album.objects.all().only("id", "name", "release_date"),
    )
)

Note that even though album__release_date field was not selected here, it got selected
in the prefetch query later. Since Django caches known objects, we have to select it here or
else it would trigger extra queries latter.

It is also possible to include hints for non-model fields using the field api or even our
@model_property (or its cached variation, @cached_model_property) decorator on the model
itself, for people who likes to keep all the business logic at the model.

For example, the following will automatically optimize only and select_related if that
field gets selected:

from strawberry_django_plus import gql

class Song(models.Model):
    name = models.CharField()

    @gql.model_property(only=["name", "album__name"], select_related=["album"])
    def name_with_album(self) -> List[str]:
        return f"{self.album.name}: {self.name}"

@gql.django.type(Song)
class SongType:
    name: auto
    name_with_album: str

Another option would be to define that on the field itself:

@gql.django.type(Song)
class SongType:
    name: auto
    name_with_album: str = gql.django.field(
        only=["name", "album__name"],
        select_related=["album"],
    )

Django Choices Enums

Convert choices fields into GraphQL enums by using
Django Choices Field extension.

from django_choices_field import TexChoicesField

class Song(models.Model):
    class Genre(models.TextChoices):
        ROCK = "rock", "Rock'n'Roll"
        METAL = "metal", "Metal"
        OTHERS = "others", "Who Cares?"

    genre = TextChoicesField(choices_enum=Genre)

In that example, a new enum called Genre will be created and be used for queries
and mutations.

If you want to name it differently, decorate the class with @gql.enum with your preferred
name so that strawberry-django-plus will not try to register it again.

Relay Support

We have a custom relay spec
implementation. It is not tied to Django at all to allow its usage with other types.

It provides types and fields for node and connection querying. For example:

# schema.py
from strawberry_django_plus import gql
from strawberry_django_plus.gql import relay

@gql.type
class Fruit(relay.Node):
    name: str

    def resolve_node(cls, node_id, info, required=False):
        ...

    def resolve_nodes(cls, node_id, info, node_ids=False):
        ...


@gql.type
class Query:
    fruit: Optional[Fruit] = relay.node()
    fruits_connection: relay.Connection[Fruit] = relay.connection()

    @relay.connection
    def fruits_connection_filtered(self, name_startswith: str) -> Iterable[Fruit]:
        # Note that this resolver is special. It should not resolve the connection, but
        # the iterable of nodes itself. Thus, any arguments defined here will be appended
        # to the query, and the pagination of the iterable returned here will be
        # automatically handled.
        ...

This will generate a schema like this:

interface Node {
  id: GlobalID!
}

type Fruit implements Node {
  id: GlobalID!
  name: String!
}

type FruitEdge implements Node {
  cursor: String!
  node: Fruit
}

type FruitConnection {
  edges: [ShipEdge!]!
  pageInfo: PageInfo!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  fruit(id: GlobalID!): Fruit
  fruits_connection(before: String, after: String, first: Int, last: Int): FruitConnection
  fruits_connection_filtered(
      before: String,
      after: String,
      first: Int,
      last: Int,
      nameStartswith: String!
  ): FruitConnection
}

It is expected that types implementing the Node interface define some methods, like
resolve_nodes and resolve_node. Take a look at
the documentation for more information.

Also note that Django fields created with @gql.django.type automatically implements
all of the required methods when the type inherits from Node.

This module also exposes a mutation that converts all of its arguments to a single input.
For example:

@gql.type
class Mutation:
    @relay.input_mutation
    def create_fruit(name: str) -> Fruit:
        ....

Will generate those types:

input CreateFruitInput {
  name: String!
}

type Mutation {
  createFruit(input: CreateFruitInput!): Fruit
}

Django Debug Toolbar Integration

Install Django Debug Toolbar
and change its middleware from:

MIDDLEWARE = [
    ...
    "debug_toolbar.middleware.DebugToolbarMiddleware",
    ...
]

To:

MIDDLEWARE = [
    ...
    "strawberry_django_plus.middlewares.debug_toolbar.DebugToolbarMiddleware",
    ...
]

Contributing

We use poetry to manage dependencies, to
get started follow these steps:

git clone https://github.com/blb-ventures/strawberry-django-plus
cd strawberry
poetry install
poetry run pytest

This will install all the dependencies (including dev ones) and run the tests.

Pre commit

We have a configuration for
pre-commit, to add the hook run the
following command:

pre-commit install

Licensing

The code in this project is licensed under MIT license. See LICENSE
for more information.

GitHub

View Github