I'm starting to get deeper into Strawberry than a simple Hello World, and am struggling in part for lack of insight into the execution engine.
My understanding is that the strawberry resolvers are just static methods, and the strawberry engine goes one tree level at a time, calling the resolvers at that level with the value returned by the resolver one level up. And it seems to call all resolvers at one level asynchronously, so there's the opportunity for dataloaders. It's a very primitive understanding, and I couldn't find more documentation.
So here's the problem. I want to have a schema like this (removing relay-style pagination for simplicity)
users {
user {
id,
name,
engaged,
}
}
engaged
has its own resolver, and I want to be able to filter users
by engaged
. That query would look something like:
users (filter: {engaged: true}) {
user {
id,
name,
engaged,
}
}
The problem is, I don't know how to make use of the value of engaged
when we're running the resolver for users
which is where the filtering needs to happen.
Our code looks like:
@strawberry.type
class User:
@strawberry.field
def engaged(self) -> bool:
# Contrived simplification
return self.name.startswith("abc")
@strawberry.type
class Query:
@strawberry.field
def users(root) -> list[User]:
result = query_database_for_users()
return [user for user in results if is_engaged(user)]
def is_engaged(user) -> bool:
# How do I implement this??
pass
I've tried really jankily calling the static method itself, a la
def is_engaged(user):
return User.engaged(user)
Which works in a really simple use case but sucks because now data loading is much less effective unless I add asynchronicity myself, and it feels like I'm reimplementing the execution engine.
Relatedly, I'm struggling to figure out how resolvers can make use of values in their sibling resolvers. I can ask this in a different question if that would be better. Extending my previous example:
@strawberry.type
class Address:
id: str
zip_code: int
@strawberry.field
def shipping_time(self) -> int:
# This is simple enough that it doesn't need a resolver, but imagine it does.
return self.zip_code // 10000
@strawberry.type
class User:
@strawberry.field
def home_address(self) -> Address:
return lookup_home_address_by_id(self.id)
@strawberry.field(self):
def work_address(self) -> Address:
return lookup_work_address_by_id(self.id)
@strawberry.field
def shipping_time(self) -> int:
# TODO(): Return min shipping time between home and work address zip codes
# Can't use my janky Address.shipping_time(address) here because I don't have the
# address yet, since it's resolved in a sibling. I reallllllyyy don't want to do
# Address.shipping_time(User.home_address(self)) because this just doesn't extend well if
# need a field many levels deep in Address.
pass
The reason I feel like this is related is because fundamentally I don't understand how resolvers are supposed to express complex logic that makes use of either sibling or child resolvers, and so I can't figure out how to express modularity without essentially implementing my own execution engine.
EDIT: Turns out part of the reason for my struggles is because I was unknowingly using a somewhat advanced feature of Strawberry that allows returned objects of resolvers to not actually be the same type as "self" would imply, via the strawberry-sqlalchemy-mapper library. If I do some type munging to make sure the type is correct, doing things like self.home_address().shipping_time()
works, but I still feel like I'm not taking advantage of the execution engine and am going to struggle with latency. Dataloaders and their included caching would definitely help, but there's no longer nice optimal DAG execution. So, this "works" but it doesn't feel right.