strawberry-graphql / strawberry-django

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

SynchronousOnlyOperation execption since Version 0.46.2 #616

Closed MaehMaeh closed 2 months ago

MaehMaeh commented 3 months ago

Describe the Bug

If I want to access a foreign key in a resolver, I get this error since version 0.46.2: django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

System Information

Additional Context

Here is my implementation:

# book.py
async def resolve_books(info: Info) -> book_types.BooksResult:
    try:
        user = info.context.get("request").consumer.scope.get("user")
        result = await sync_to_async(services.book.all)(user=user)
        return book_types.Books(books=result)
    except PermissionDenied as err:
        return PermissionProblem(code=err.code, message=err.message)

@strawberry.type
class BookQueries:
    books: book_types.BooksResult = strawberry.field(resolver=resolve_books)

# book_types.py
from core.graphql.author import author_resolver

BooksResult = Annotated[
    Union[Books, PermissionProblem],
    strawberry.union("BooksResult"),
]

@strawberry_django.type(
    model=models.Book, filters=BookFilter, order=BookOrder, pagination=True
)
class Book:
    id: auto
    name: auto
    author: AuthorResult = strawberry.field(resolver=author_resolver)

# author.py
def author_resolver(root, info: Info) -> author_types.AuthorResult:
    id_ = root.author_id
    return resolve_author(id=id_, info=info)

async def resolve_author(id: uuid.UUID, info: Info) -> author_types.AuthorResult:
    try:
        user = info.context.get("request").consumer.scope.get("user")
        result = await sync_to_async(services.author.get)(
            user=user,
            id=id,
        )
        return result
    except PermissionDenied as err:
        return PermissionProblem(code=err.code, message=err.message, object_id=id)
    except DoesNotExist as err:
        return DoesNotExistProblem(code=err.code, message=err.message, object_id=id)

The problem is the call root.author_id in def author_resolver. In version 0.46.2 the author_id was already loaded at this point. This is why there was no error message.

What is the best way to change my implementation so that it works again with the current version?

Upvote & Fund

Fund with Polar

bellini666 commented 2 months ago

Hi @MaehMaeh ,

Yes, this got changed in this PR to fix some issues with custom resolvers.

In summary, when you define a custom resolver for a field, you need to provide hints for anything that you are going to access inside it. That means that the fix would be to do this:

@strawberry_django.type(
    model=models.Book, filters=BookFilter, order=BookOrder, pagination=True
)
class Book:
    id: auto
    name: auto
    author: AuthorResult = strawberry_django.field(
        resolver=author_resolver,
        only=["author_id"],
    )

On a side note, you could simply type this directly and the optimizer would take care of doing select_related for you. Is there a reason why you are using a custom prefetch in this case?

bellini666 commented 2 months ago

Going to close this as the question is probably already answered, but please let me know if we need to reopen it for some reason

MaehMaeh commented 2 months ago

First of all, a big thank you for solving my problem.

About the reason: The idea behind the structure is that I would like to have the option of returning a union for each object. So in this example Author or PermissionProblem. This should also be possible in the subquery. This should make such queries possible:

query Books {
  books {
    ... on Books {
      books(
        order: {creator: {email: ASC}}
        filters: {creator: {email: {contains: "rwt"}}}
        pagination: {limit: 3, offset: 0}
      ) {
        id
        name
        author {
          ... on Author {
            name
          }
          ... on PermissionProblem {
            objectId
            code
            message
          }
        }
      }
    }
    ... on PermissionProblem {
      objectId
      code
      message
    }
  }
}

My permission check takes place exclusively in the layer below the GraphQL layer, as several “front ends” can access the back end. I'm beginning to regret my approach too. And of course it could easily be that there could be an easier, more sensible variant.