strawberry-graphql / strawberry-django

Strawberry GraphQL Django extension
https://strawberry.rocks/docs/django
MIT License
421 stars 122 forks source link

Migrating from django-graphene-cud #83

Open ghost opened 2 years ago

ghost commented 2 years ago

We are looking into moving away from graphene and strawberry looks like a very nice alternative!

Unfortunately we have some heavy integration with django-graphene-cud which offers a lot of useful features (some are too magic for my liking). I was unable to find a way to use this package in a way that offered us the hooks we needed for some of our mutations and ended up on a boilerplate heavy strawberry only PoC shown below.

Are we missing anything obvious with this library?

import strawberry
from typing import Optional
from django.db.models.query_utils import Q
from strawberry.permission import BasePermission
from strawberry.types.info import Info
from foo.graphql.types import FooNode
from foo.models import Foo
from project.graphql.context import PermissionsContext

class IsBar(BasePermission):
    message = "User must be bar"

    def has_permission(self, source, info: Info, **kwargs) -> bool:
        return info.context.is_bar

@strawberry.interface
class FooFields:
    code: Optional[str] = None
    description: Optional[str] = None

@strawberry.input
class CreateFooInput(FooFields):
    def save(self, info: Info):
        fields = self.__dict__
        fields["owner"] = info.context.user
        foo = Foo.objects.create(**fields)
        return foo

@strawberry.input
class PatchFooInput(FooFields):
    def patch(self, info: Info, id: str):
        fields = self.__dict__
        permissions: PermissionsContext = info.context
        filter = Q(owner=permissions.user)
        if permissions.is_company_admin:
            filter |= Q(owner__company_id=permissions.company_id)
        queryset = Foo.objects.filter(id=id).filter(filter)
        queryset.update(**fields)
        return queryset.get()

@strawberry.type
class Mutation:
    @strawberry.field(permission_classes=[IsEmployed])
    def create_foo(self, info: Info, input: CreateFooInput) -> FooNode:
        return input.save(info)

    @strawberry.field(permission_classes=[IsEmployed])
    def update_foo(self, info: Info, id: str, input: PatchFooInput) -> FooNode:
        return input.patch(info, id)

Upvote & Fund

Fund with Polar

la4de commented 2 years ago

Do you have any specific questions you would like to get answer? Your example code looks quite generic, which means that you probably want to ask some of your questions in strawberry core project: https://github.com/strawberry-graphql/strawberry

ghost commented 2 years ago

It was suggested I ask on here when I asked on discord.

My question is, how can I hook into the mutations.* utilities so that I can add business logic, rather than having to reimplement them with my custom logic. For example, when creating a Foo I might want to populate an owner field based on the request context. Or If I am updating Bar I might want to check user has the correct permissions or perform certain business logic dependent on state.

la4de commented 2 years ago

You can extend existing mutation classes and add your business logic there. Unfortunately I cannot verify the example code just now but you get the idea

from strawberry_django.mutations.fields import DjangoUpdateMutation

class YourUpdateMutation(DjangoUpdateMutation):
    def resolver(self, info, source, data, **kwargs):
        queryset = super().resolver(info, source, data, **kwargs)
        queryset.update(owner=...)
        return queryset

@strawberry.type
class Mutation:
    update_foo: List[FooNode] = YourUpdateMutation(PatchFooInput)
sjdemartini commented 1 year ago

I too have heavy usage of graphene-django-cud and find it to be critical for reducing a ton of boilerplate, so would not be able to migrate from graphene to strawberry without some equivalent.

I recently came across strawberry-django-extras which seems to support nested mutations and presumably help with DRYing up mutation code. Haven't used it myself yet but looks promising!

lyndseyjw commented 4 months ago

@la4de is right, you want to use strawberry_django's DjangoUpdateMutation, except you want to use the update method .. in your case it would look something like this:

from strawberry_django.mutations.fields import DjangoUpdateMutation
from strawberry_django.permissions import IsStaff

class YourUpdateMutation(DjangoUpdateMutation):
    def update(self, info, instance, data):
        # ...business logic... I found you can pretty much copy & paste the logic you used with graphene-django-cud #

        self.key_attr = "pk"
        self.full_clean = True
        return super().update(info, instance, data)

class YourCreateMutation(DjangoCreateMutation):
    def create(self, data, info):
        # ...business logic... #

        return super().create(data, info=info)

@strawberry.type
class Mutation:
    update_foo: List[FooNode] = YourUpdateMutation(PatchFooInputPartial, extensions=[IsStaff()])
    create_foo: List[FooNode] = YourCreateMutation(CreateFooInput, extensions=[IsStaff()])

also wanted to suggest the use of IsStaff in strawberry_django.permissions as it might solve for the permissions logic you're trying to achieve