2

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.

Matt
  • 21
  • 2

1 Answers1

0

One technique would be to use cached properties, either on the type itself or an internal domain object.

@strawberry.type
class User:
    name: str

    @strawberry.field
    def engaged(self) -> bool:
        return self._engaged

    @cached_property
    def _engaged(self) -> bool:
        return self.name.startswith("abc")
A. Coady
  • 54,452
  • 8
  • 34
  • 40