Skip to main content

Enhanced Strawberry GraphQL integration with Django

Project description

strawberry-django-plus

build status coverage downloads PyPI version python version django version

Enhanced Strawberry integration with Django.

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

Features

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

Query optimizer extension

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=[
        # other 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()

This query for the artist field:

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

Will generate an optimized query like this:

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"),
        )
    ),
)

Querying a song and its related fields like this:

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

Will generate an optimized query like this:

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) -> 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 this lib will not try to register it again.

Permissioned resolvers

Permissioning is done using schema directives by applying them to the fields that requires permission checking.

For example:

@strawberry.type
class SomeType:
    login_required_field: RetType = strawberry.field(
        # will check if the user is authenticated
        directives=[IsAuthenticated()],
    )
    perm_required_field: OtherType = strawberry.field(
        # will check if the user has `"some_app.some_perm"` permission
        directives=[HasPerm("some_app.some_perm")],
    )
    obj_perm_required_field: OtherType = strawberry.field(
        # will check the permission for the resolved value
        directives=[HasObjPerm("some_app.some_perm")],
    )

Available options are:

  • IsAuthenticated: Checks if the user is authenticated (user.is_autenticated)
  • IsStaff: Checks if the user is a staff member (user.is_staff)
  • IsSuperuser: Checks if the user is a superuser (user.is_superuser)
  • HasPerm(perms: str, list[str], any: bool = True): Checks if the user has any or all of the given permissions (user.has_perm(perm))
  • HasRootPerm(perms: str | list[str], any: bool = True): Checks if the user has any or all of the given permissions for the root of that field (user.has_perm(perm, root))
  • HasObjPerm(perms: str | list[str], any: bool = True): Resolves the retval and then checks if the user has any or all of the given permissions for that specific value (user.has_perm(perm, retval)). Note that if the return value is a list, this directive will filter the return value, removing objects that fails the check (check below for more information regarding other possibilities).

There are some important notes regarding how the directives handle the return value:

  • If the user passes the check, the retval is returned normally
  • If the user fails the check:
    • If the return type was Optional, it returns None
    • If the return type was a List, it returns an empty list
    • If the return type was a relay Connection, it returns an empty Connection
    • If the field is a union with types.OperationInfo or types.OperationMessage, that type is returned with a kind of PERMISSION, explaining why the user doesn't have permission to resolve that field.
    • Otherwise, it raises a PermissionError for that resolver, which will be available at the result's errors field.

Note that since strawberry doesn't support resolvers for schema directives, it is necessary to use this lib's custom extension that handles the resolution of those and any other custom defined schema directive inherited from strawberry_django_plus.directives.SchemaDirectiveResolver:

import strawberry
from strawberry_django_plus.directives import SchemaDirectiveExtension

schema = strawberry.Schema(
    Query,
    extensions=[
        SchemaDirectiveExtension,
        # other extensions...
    ]
)

Django mutations

This lib provides 3 CRUD mutations for create/update/delete operations, and also a facility for creating custom mutations with automatic ValidationError support.

CRUD mutations

  • gql.django.create_mutation: Will create the model using the data from the given input, returning a types.OperationInfo if it fails with all raised ValidationError data.
  • gql.django.update_mutation: Will update the model using the data from the given input, returning a types.OperationInfo if it fails with all raised ValidationError data.
  • gql.django.delete_mutation: Will delete the model using the id from the given input, returning a types.OperationInfo if it fails with all raised ValidationError data.

A simple complete example would be:

from strawberry_django_plus import gql

@gql.django.type(SomeModel)
class SomeModelType(gql.Node):
    name: gql.auto

@gql.django.input(SomeModelType)
class SomeModelInput:
    name: gql.auto


@gql.django.partial(SomeModelType)
class SomeModelInputPartial(gql.NodeInput):
    name: gql.auto

@gql.type
class Mutation:
    create_model: SomeModelType = gql.django.create_mutation(SomeModelInput)
    update_model: SomeModelType = gql.django.update_mutation(SomeModelInputPartial)
    delete_model: SomeModelType = gql.django.delete_mutation(gql.NodeInput)

Custom model mutations

It is possible to create custom model mutations with gql.django.input_mutation, which will automatically convert the arguments to a input type and mark the return value as a union between the type annotation and types.OperationInfo. The later will be returned if the resolver raises ValidationError.

For example:

from django.core.exceptions import ValidationError
from strawberry_django_plus import gql

@gql.type
class Mutation:
    @gql.django.input_mutation
    def set_model_name(self, info, id: GlobalID, name: str) -> ModelType:
        obj = id.resolve_node(info)
        if obj.some_field == "some_value":
            raise ValidationError("Cannot update obj with some_value")

        obj.name = name
        obj.save()
        return obj

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.
        ...

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. By default the id field is used for the node id. This is customizable with the id_attr attribute. 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. By default the pk field is used for the node id (overwrites the default id field) but can also be customized with the id_attrattribute.

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.

Stats

Recent Activity

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

strawberry-django-plus-1.14.2.tar.gz (66.3 kB view details)

Uploaded Source

Built Distribution

strawberry_django_plus-1.14.2-py3-none-any.whl (71.7 kB view details)

Uploaded Python 3

File details

Details for the file strawberry-django-plus-1.14.2.tar.gz.

File metadata

  • Download URL: strawberry-django-plus-1.14.2.tar.gz
  • Upload date:
  • Size: 66.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.8.2 Linux/5.13.0-1029-azure

File hashes

Hashes for strawberry-django-plus-1.14.2.tar.gz
Algorithm Hash digest
SHA256 d2a1471102089f7f6b164d76f62d1074ce1307b1071a169a03f4b10602456717
MD5 05ff0025c9a8985bae84c550051f8d78
BLAKE2b-256 a07ce9da65e11b9fa3124f804c5a5b7e16d9b6357b3f68e3cb5b7672b3ae8946

See more details on using hashes here.

File details

Details for the file strawberry_django_plus-1.14.2-py3-none-any.whl.

File metadata

File hashes

Hashes for strawberry_django_plus-1.14.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a8eeee774f55005867e09f240048cb324e0cc3800deb2af1c187ebd4198d56e0
MD5 c0fbc5ed7c4b55e2ffc4ec9533539d39
BLAKE2b-256 94081877a64dd010e8c28045942c6b1b7a2ddee11cef94ee8bfc34eeadf3e610

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page