Currently, Strawberry lacks a mechanism for extending types with reusable logic, such as pagination, database model mappers for libraries like sqlalchemy, or compatibility extensions like strawberry.experimental.pydantic. Much of the logic necessary for these to work has to be written separately for each use case. Opinionating the way we extend strawberry types could standardize behavior and improve compatibility and maintainability. This issue proposes an ObjectTypeExtension
API that provides a foundation to address the mentioned points.
ObjectTypeExtension
s provide custom functionality, can modify the underlying TypeDefinition
or may offer the possibility to implement standardized hooks (more on that later).
class ObjectTypeExtension(ABC):
def apply(strawberry_type: TypeDefinition):
"""
In this method, modifications can be made to TypeDefinition.
Custom fields or directives can be added here. It will be called
after the TypeDefinition is initialized
"""
pass
A DjangoModelExtension
could use apply
to setup all the automatic model fields, similar to a PydanticModelExtension
or SQLAlchemyModelExtension
Similar to Field Extensions (#2168 , #2567), ObjectTypeExtension
instances are passed to the @strawberry.type
annotation:
@strawberry.type(extensions=[DjangoModelExtension(model=CarModel)])
class Car:
...
will resolve to:
type Car{
id: ID!
manufacturer: String!
model: String!
doors: Int!
}
Using the annotation to define extensions is favorable over polymorphism of the actual MyType
class, as that is a dataclass
with resolver logic. The Extensions will provide behavioral logic and extended functionality and are a better fit for StrawberryType
. Extensions themselves support polymorphism. The DjangoExtension
could natively support OffsetPaginationExtension
or RelayPaginationExtension
.
Extensions are initialized after TypeDefinition
initialization. Additionally, we can provide helper methods to make dealing with polymorphic extensions easier:
@dataclass
class TypeDefinition(StrawberrryType):
extensions: List[ObjectTypeExtension]
def __post_init__():
for extension in self.extensions:
extension.apply(self)
...
#rest of current post init
def get_extension(extension_type: Type[ObjectTypeExtension]):
# extensions can be polymorphic (DjangoModelExtension can inherit from PaginationExtension...)
return next(filter(self.extensions, lambda x: isinstance(x, extension_type)))
A major API to interact with extensions will be the FieldExtension
API. In cases like Pagination, FieldExtensions
have a synergy with ObjectTypeExtensions
by defining the user-facing pagination logic on the FieldExtension
and using the ObjectTypeExtension
to actually resolve the data. This way, only one FieldExtension
is necessary to implement OffsetPagination
, which is compatible with both DjangoModelExtension
, SQLAlchemyModelExtension
and more:
class DjangoModelExtension(OffsetPaginatableTypeExtension, RelayTypeExtension):
def __init__(model: DjangoModel)
def resolve_offset_paginated_items(offset, limit):
# all the django db logic here
@strawberry.type
class Query
cars: list[Car] = strawberry.field(extensions=[OffsetPaginatedFieldExtension(default_limit=100)])
relayed_cars: list[Car] = strawberry.field(extensions=[RelayPaginatedFieldExtension()])
resolves to
type Query {
cars(offset: Int, limit: Int = 100): [Car!]!
relayedCars(
before: String
after: String
first: Int
last: Int
): CarConnection!
}
using
class OffsetPaginatedFieldExtension(FieldExtension):
def apply(field: StrawberryField):
assert isinstance(field.type, StrawberryList)
# side note: some helpers may be useful here
listed_type : TypeDefinition = get_nested_type_defintion(field)
# Returns DjangoModelExtension in this case
self.offset_pagination_extension = listed_type.get_extension(OffsetPaginatableTypeExtension)
assert offset_pagination_extension
# add necessary arguments etc
def resolve(info, offset, limit):
# shortened
return self.offset_pagination_extension.resolve_offset_paginated_items(info, offset, limit)
Using this approach streamlines user-facing behavior and helps standardize the internal logic for all extensions of strawberry. Filtering or sorting are other great options to use the combination of FieldExtensions
and ObjectTypeExtensions
Dealing with type hints
We need to decide how to handle automatic database model-derived fields. Should we require strawberry.auto
-typed fields on the actual types (similar to the current pydantic extension), or can the user just pass an empty object type? strawberry.auto
provides little benefit to the user in case of manual use, as it will not reveal any type information. As such, it might be better to only enfore its use in override-cases (e.g. change the default description, type or alias a model field)
Implement hooks
Hooks such as wrap_resolve
wrapping the resolver of each object type could provide additonal on-resolve functionality.
Cases like unnecessary database calls just to resolve an ID field may be avoided using wrap_resolve
by parsing the selection_set
before any resolver is actually called. However, their use might be an antipattern to the proposed ObjectTypeExtension
+ FieldExtension
synergy as it's easier and more explicit to implement that using FieldExtension
s. My personal preference is to not provide standardized resolve-time hooks and implement that functionality using FieldExtension
s instead.
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too