strawberry-graphql / strawberry

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

Support for "FieldResolver" pattern to resolver circular dependencies #2524

Open twavv opened 1 year ago

twavv commented 1 year ago

Circular dependencies seem to be a common issue with Strawberry.

I'm wondering if Strawberry would consider adding support for something like type-graphql's FieldResolvers. That allows one to define fields for other types outside of the immediate class definition of that type.

For example, consider a schema with Person and Pet objects. A Person might have a pets: [Pet!]! field and each pet might have a owners: [Person!]! field. This requires Person and Pet to "know" about each other.

With type-graphql, one can define

// person.ts
@ObjectType()
class Person {
  @Field()
  get name(): string {
    // ...
  }

  // other fields
}

and

// pet.ts
import { Person } from "./person";

@ObjectType()
class Pet {
  @Field()
  name: string;

  // other pet fields

  @Field()
  owners(): Person[] {
    // ...
  }
}

@Resolver(of => Person)
class PersonPetResolver {
  @Field()
  pets(
    @Root() person: Person,
  ): Person[] {
    // ...
  }
}

In Strawberry land, this might look something like

# person.py

@strawberry.type
class Person:
  name: str
  # ...

and

# pet.py

from .person import Person

@strawberry.type
class Pet:
  name: str
  # ...

  @strawberry.field
  def owners(self) -> List[Person]:
    ...

  # This could either be a staticmethod or just a decorator on a normal (non-method) function
  @strawberry.resolver
  @staticmethod
  def person_pets(root: Person) -> List[Pet]:
    ...

Thoughts? Am I missing something?

Upvote & Fund

Fund with Polar

mecampbellsoup commented 3 months ago

https://strawberry.rocks/docs/faq#how-can-i-deal-with-circular-imports

Does this help?

Travis-Taylor commented 1 week ago

I think the FAQ blurb about circular imports is missing a (IMO pretty important) case where you have a strawberry.field() that specifies a resolver function. I'll share some examples from my own project:

# cargo.py
def get_cargo_flights(
    cargo_uuid: UUID,
) -> List[Annotated["FlightResult",
    strawberry.lazy("flight_pipeline.api.flights")]]
]:
    """Get all flights associated with the given cargo"""
    query_result = Flights().get_cargo_flights(cargo_id=cargo_uuid)
    result = []
    for flight in query_result:
        result.append(FlightResult.from_db_model(flight)) # <--- still need a concrete FlightResult here
    return result
...
@strawberry.type
class CargoResult:
    uuid: UUID
    bag_tag: str
    flights: List[Annotated["FlightResult",
        strawberry.lazy("flight_pipeline.api.flights")]
    ] = strawberry.field(
        resolver=lambda root: get_cargo_flights(cargo_uuid=root.uuid)
    )
    ...

and likewise for flights:

# flights.py
from flight_pipeline.api.cargo import CargoResult

def get_flight_cargo(
    flight_uuid: UUID
) -> List[Annotated["CargoResult",
        strawberry.lazy("flight_pipeline.api.cargo")]]:
    """Get all cargo associated with the given flight"""
    query_result = Cargo().get_flight_cargo(flight_id=flight_uuid)
    result = []
    for cargo in query_result:
        result.append(CargoResult.from_db_model(cargo))
    return result

class FlightResult:
    uuid: UUID
    flight_number: str
    cargo: List[Annotated["CargoResult",
        strawberry.lazy("flight_pipeline.api.cargo")]
    ] = strawberry.field(
        resolver=lambda root: get_flight_cargo(flight_uuid=root.uuid)
    )
    ...

I was eventually able to find a (seemingly hacky) solution: move the from flight_pipeline.api.flights import FlightResult to the very bottom of cargo.py (and leave the from flight_pipeline.api.cargo import CargoResult at the top of flights.py); this seems to evaluate the import late enough that the circular import error is avoided. But it seems like this is not a reliable solution...