strawberry
strawberry copied to clipboard
Object Type Extensions
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.
API proposal
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!
}
Extensions and Polymorphism
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
.
Initialization
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)))
Interacting with Object Type Extensions
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
Decisions
-
[ ] 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 usingwrap_resolve
by parsing theselection_set
before any resolver is actually called. However, their use might be an antipattern to the proposedObjectTypeExtension
+FieldExtension
synergy as it's easier and more explicit to implement that usingFieldExtension
s. My personal preference is to not provide standardized resolve-time hooks and implement that functionality usingFieldExtension
s instead.
Upvote & Fund
- We're using Polar.sh so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.