Skip to content

Quick Start

Installation

pip install strawberry-django-extras


JWT Authentication

The JWT part of this package is heavily based on the django-graphql-jwt package. Noting that, not all features supported by that package are supported here. For details on what is supported please refer to the the JWT part of these docs.


Add the JWT Authentication Backend to your settings

settings.py
1
2
3
4
  AUTHENTICATION_BACKENDS = [
    'strawberry_django_extras.jwt.backend.JWTBackend',
    'django.contrib.auth.backends.ModelBackend',
]


Add the JWT Middleware to your settings

settings.py
1
2
3
4
  MIDDLEWARE = [
    ...
    "strawberry_django_extras.jwt.middleware.jwt_middleware",
]

Note

This implementation of the middleware is different from other implementations in that it is designed to handle token based authentication for all request containing the Authorization header regardless of wether the request is to be consumed by your GraphQL view. This aims to provide a unified way of authenticating via JWT tokens issued by this package accross your entire application.

If the request contains the Authorization header and the token is valid, the user will be authenticated and the request.user will be set to the user associated with the token. If the token is expired a 401 response will be returned along with Token Expired. If an invalid token is provided a 401 response will be returned along with Invalid Token.

If the user is already authenticated by some previous middleware in your middleware stack it will be respected and the token will not be checked at all. The order of the middleware in your middleware stack is important and you can set it depending on your needs.


Expose the mutations in your GraphQL schema

schema.py
1
2
3
4
5
6
7
from strawberry_django_extras.jwt.mutations import JWTMutations

@strawberry.type
class Mutation:
    request_token = JWTMutations.issue
    revoke_token = JWTMutations.revoke
    verify_token = JWTMutations.verify


Override any settings you might need in your project settings.

settings.py
GRAPHQL_JWT = {
    'JWT_ALGORITHM': 'EdDSA',
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LONG_RUNNING_REFRESH_TOKEN': True,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=5)
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
    'JWT_AUTHENTICATE_INTROSPECTION': True,
    'JWT_REFRESH_TOKEN_N_BYTES': 64,
    'JWT_PRIVATE_KEY': base64.b64decode('YOUR_PRIVATE_KEY'),
    'JWT_PUBLIC_KEY': base64.b64decode('YOUR_PUBLIC_KEY')
}

If you set JWT_LONG_RUNNING_REFRESH_TOKEN to True you will need to add the following to your settings file:

settings.py
1
2
3
4
INSTALLED_APPS = [
    ...
    'strawberry_django_extras.jwt.refresh_token.apps.RefreshTokenConfig',
]

and run python manage.py migrate to create the refresh token model.

If you set JWT_AUTHENTICATE_INTROSPECTION to True you will need to add an extension to the root of your schema:

schema.py
from strawberry_django_extras.jwt.extensions import DisableAnonymousIntrospection

schema = strawberry.Schema(
    query=Query,
    mutation=Mutation,
    subscription=Subscription,
    extensions=[
        DisableAnonymousIntrospection,
        ...
    ]
)


Mutation Hooks

Mutation hooks are provided via a field_extension and can be applied to any strawberry mutation.

hooks.py
1
2
3
4
5
6
7
8
9
def update_user_pre(info: Info, mutation_input: UserUpdateInput):
    mutation_input.lastname = mutation_input.lastname.lower()

async def update_user_post(
    info: Info, 
    mutation_input: UserUpdateInput,
    result: Any
):
    await log(f'User {result.id} updated')

and then applied to your mutation:

schema.py
from strawberry_django_extras.field_extensions import mutation_hooks
from strawberry_django import mutations
from .hooks import update_user_pre, update_user_post

@strawberry.type
class Mutation:
    update_user: UserType = mutations.update(
        UserInputPartial,
        extensions=[
            mutation_hooks(
                pre=update_user_pre,
                post_async=update_user_post
            )
        ]
    )

Note

You might have noticed that we are passing both a sync and an async function at the same time. This is possible because if the context is async the sync function will be wrapped with sync_to_async and awaited. If the context is sync passing post_async and pre_async will be ignored. In either case the async functions are awaited.


Input Validations

Much inspired by the way graphene-django-cud handles validation, this package provides a similar way to validate your input when the respective input classes are instantiated.

inputs.py
@strawberry_django.input(get_user_model())
class UserInput:
    firstname: auto
    lastname: auto

    def validate(self, info):
        if self.firstname == self.lastname:
            raise ValidationError(
                "Firstname and lastname cannot be the same"
            )

    # Or for individual fields    
    def validate_lastname(self, info, value):
        if value == self.firstname:
            raise ValidationError(
                "Firstname and lastname cannot be the same"
            )

When updating an existing object the pk will be available through self.id so you can validate values in comparison to existing ones. Also info is provided in case validations need to be run against the user making the request.

Finally add this to each mutation that needs to run validations:

schema.py
1
2
3
4
5
6
7
8
9
from strawberry_django_extras.field_extensions import with_validation
from strawberry_django import mutations

@strawberry.type
class Mutation:
    create_user: UserType = mutations.create(
        UserInput,
        extensions=[with_validation()]
    )


Permissions

Similarly to validations, permission checking is run on input instantiation. Since strawberry does not currently provide a way to pass permission_classes to input fields this package allows you to write your permission checking functions as part of the input class.

inputs.py
@strawberry_django.input(get_user_model())
class UserInput:
    firstname: auto
    lastname: auto

    def check_permissions(self, info):
        if not info.context.user.is_staff:
            raise PermissionDenied(
                "You need to be staff to do this"
            )

    # Or for individual fields    
    def check_permissions_lastname(self, info, value):
        if value == info.context.user.lastname:
            raise PermissionDenied(
                "You cannot create a user with the same lastname as you"
            )
schema.py
1
2
3
4
5
6
7
8
9
from strawberry_django_extras.field_extensions import with_permissions
from strawberry_django import mutations

@strawberry.type
class Mutation:
    create_user: UserType = mutations.create(
        UserInput,
        extensions=[with_permissions()]
    )

Note

As documented by Strawberry extension order does matter so make sure you are passing the with_permissions() and with_validation() extensions in an order that makes sense for your project.


Nested Mutations

This package provides support for deeply nested mutations through a field extension and some wrapper input classes.

It makes sense for the inputs to be different when updating an object vs creating one. So we provide different input wrappers for each type of operation. It also makes sense that the api provided would be different depending on the type of relationship between the related models. Brief explanations of each input wrapper is provided below. For details refer to relevant guide on nested mutations.

Wrappers for nested objects for create mutations

One to One

CRUDOneToOneCreateInput can be used when you want to create or assign a related object, alongside the creation of your root object. The resulting schema will provide two actions for your mutation create and assign which are mutually exclusive. create is of type UserCreateInput which you will need to provide and assign is of type ID. A brief example follows.

models.py
    class User(AbstractBaseUser, PermissionsMixin):
        firstname = models.CharField(max_length=30, blank=True)
        lastname = models.CharField(max_length=30, blank=True)
        email = models.EmailField(max_length=254, unique=True)   
        USERNAME_FIELD = 'email'
        EMAIL_FIELD = 'email'

    class Goat(models.Model):
        name = models.CharField(max_length=255, default=None, null=True, blank=True)
        user = models.OneToOneField(User, related_name="goat", on_delete=models.CASCADE, null=True, default=None)
inputs.py
    from strawberry_django_extras.inputs import CRUDOneToOneCreateInput

    @strawberry_django.input(get_user_model())
    class UserInput:
        firstname: auto
        lastname: auto
        email: auto
        password: auto
        goat: Optional[CRUDOneToOneCreateInput[GoatInput]] = UNSET

    @strawberry_django.input(Goat)
    class GoatInput:
        name: auto
        user: Optional[CRUDOneToOneCreateInput['UserInput']] = UNSET
schema.py
    from strawberry_django_extras.field_extensions import with_cud_relationships
    from strawberry_django import mutations

    @strawberry.type
    class Mutation:
        create_user: UserType = mutations.create(
            UserInput,
            extensions=[with_cud_relationships()]
        )

        create_goat: GoatType = mutations.create(
            GoatInput,
            extensions=[with_cud_relationships()]
        )

Now we can create or assign nested objects on either side of the relationship.

mutation {
    createGoat(data: {
        name: "Marina"
        user: {
            create: {
                firstname: "Lakis"
                lastname: "Lalakis"
                email: "lalakis@domaim.tld"
                password: "abc"
            }
        }
    }) {
        id
        name
        user {
            id
            lastname
            firstname
            email
        }
    }
}

One to Many

CRUDOneToManyCreateInput. Similarly to the one to one wrapper it provides two actions create and assign which are mutually exclusive. The only difference is that the relationship is through a ForeignKey meaning the other side of the relationship would be Many to One requiring a different wrapper.

Many to One

CRUDManyToOneCreateInput. This wrapper is used when the relationship is Many to One. It provides two actions create and assign which are NOT mutually exclusive. The inputs are of course lists and of type SomeModelInput and ID respectively.

Many to Many

CRUDManyToManyCreateInput is provided for Many-to-Many relationships. It provides two actions create and assign which are NOT mutually exclusive. The inputs are ofcourse lists again but there's one important difference. They are internally wrapped again to provide a mechanism for the user to provide through_defaults for the relationship either on assignment or creation. The type for through_defaults is JSON and the values should follow snake case.

Wrappers for nested objects for update mutations

These wrappers expect two inputs to be provided instead of the one that was necessary for creation. The first is for creation of new related objects when updating the current object and the second is for updates to the data of already related objects.

One to One

CRUDOneToOneUpdateInput can be used when alongside an update mutation you want to update related objects. The resulting schema will provide three possible actions for your mutation and a boolean flag.

  • create of type UserInput used to create a new related object.
  • assign of type ID used to assign an existing objects as related.

    Note that you can use null to remove the relationship if the field is nullable.

  • update of type UserPartial used to update the fields of an existing related object.
  • delete of type bool indicating whether the related object should be deleted.

    Note that this flag can be used together with assign or create to delete the previously related object.

One to Many

CRUDOneToManyUpdateInput can be used when alongside an update mutation you want to update related objects. The resulting schema will provide three possible actions for your mutation and a boolean flag. These are the same as the ones provided by the One to One wrapper, and they function in exactly the same fashion.

For a more detailed explanation with examples please refer to the relevant guide on nested mutations.

Many to One

CRUDManyToOneUpdateInput can be used when alongside an update mutation you want to update related objects. The resulting schema will provide four possible actions for your mutation. These are as follows:

  • create of type List[UserInput] used to create new related objects.
  • assign of type List[ID] used to assign relations with existing objects.
  • update of type List[UserPartial] used to update the fields of existing related objects.
  • remove of type List[CRUDRemoveInput] which wraps an ID and a bool flag indicating whether the removed object should be deleted.

Many to Many

CRUDManyToManyUpdateInput can be used when alongside an update mutation you want to update related objects. The resulting schema will provide four possible actions for your mutation. These are as follows:

  • create of type List[CRUDManyToManyItem] which wraps two inputs.
    • objectData of type UserInput used for the fields of the related object.
    • throughDefaults of type JSON used for any data stored in the through model if one exists.
  • assign of type List[CRUDManyToManyID] which wraps two inputs.
    • id of type ID used to assign relations with existing objects.
    • throughDefaults of type JSON used for any data stored in the through model if one exists.
  • update of type List[CRUDManyToManyItemUpdate] which wraps two inputs.
    • objectData of type UserPartial used to update the fields of the related object.
    • throughDefaults of type JSON used to update the fields of the through model if one exists.
  • remove of type List[CRUDRemoveInput] which wraps an ID and a bool flag indicating whether the removed object should be deleted.

For a more detailed explanation with examples please refer to the relevant guide on nested mutations.