strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations 🍓
https://strawberry.rocks
MIT License
3.95k stars 525 forks source link

Lazy resolvers? #3550

Open mecampbellsoup opened 3 months ago

mecampbellsoup commented 3 months ago

We are trying to make use of nested resolvers such that the resolvers that comprise our graph are more composable and independent of one another.

Before/what we have currently:

from cloud_console.gql.resolvers.access_token import access_tokens
from cloud_console.gql.resolvers.groups import groups
from cloud_console.gql.resolvers.organization import organization

@strawberry.experimental.pydantic.type(model=CurrentUserPyd, all_fields=True)
class CurrentUser:
    access_tokens = access_tokens
    groups = groups
    organization = organization

After/what we want to move towards:

@strawberry.experimental.pydantic.type(model=CurrentUserPyd, all_fields=True)
class CurrentUser:
    @strawberry.field
    async def access_tokens(self, info: strawberry.Info) -> list[AccessToken] | None:
        from cloud_console.gql.resolvers.access_token import access_tokens

        return await access_tokens(info)

    @strawberry.field
    async def groups(self, info: strawberry.Info) -> list[Group] | None:
        from cloud_console.gql.resolvers.groups import groups

        return await groups(info)

    @strawberry.field
    async def organization(self, info: strawberry.Info) -> Organization | None:
        from cloud_console.gql.resolvers.organization import organization

        return await organization(info)

Is there anything we can do to avoid having to put the resolver imports in the field func definitions? Feels very not Pythonic to establish this pattern.

cc @patrick91

Upvote & Fund

Fund with Polar

patrick91 commented 3 months ago

@mecampbellsoup do you see any alternatives on how this could work? 😊

erikwrede commented 3 months ago

I'm having some trouble understanding your case. Can you provide a detailed example? Are you looking for some way to handle shared business logic or dependency injection with strawberry to have lean resolvers? Some extra information on why your provided to-be example is currently infeasible for you would really help :)

mecampbellsoup commented 3 months ago

@mecampbellsoup do you see any alternatives on how this could work? 😊

We could probably have all of our types and resolvers defined in one giant module! Then we could use strawberry.lazy for deferred type annotations.

Are you looking for some way to handle shared business logic or dependency injection with strawberry to have lean resolvers? Some extra information on why your provided to-be example is currently infeasible for you would really help :)

That code was lifted directly out of our app, FWIW. I am personally reluctant to lean into a pattern of moving our resolver imports from the top of the module into the field definitions/functions. I was curious whether other people using strawberry have encountered a similar issue (going from one-way resolvers that import models/types, to two-way models-resolvers imports.

To clarify, in the 2nd code snippet, our resolver function imports are moved into the fields because now that those functions return other types like AccessToken and Group (which are also imported in cloud_console.gql.resolvers.access_token and cloud_console.gql.resolvers.groups, respectively), we get circular dependency import errors if they are left at the top of the file/module.

Does that make sense or help clarify?

rohan-mehta commented 2 months ago

Also having the same issue with circular dependencies. One idea I had was to allow adding fields to a strawberry type via an API. Full example in #3577, but sharing here:

# user.py
import strawberry

@strawberry.type
class User:
    name: str

# post.py
import strawberry

@strawberry.type
class Post:
    text: str
    uid: strawberry.Private[str]

# user_extension.py
import strawberry
from .user import User
from .post import Post

async def fetch_recent_post(user: User) -> Post:
    ...

async def extend_user():
    User.add_field(
        "recent_post",
        fetch_recent_post
    )

# post_extension.py

import strawberry
from .user import User
from .post import Post

async def fetch_user(post: Post, uid: str) -> User:
    ...

async def extend_post():
    Post.add_field(
        "owner",
        fetch_user,
    )

# root_schema.py
from .post_extension import extend_post
from .user_extension import extend_user

schema = strawberry.Schema(query=Query, extend_with = [extend_user, extend_post])