Skip to content

Instantly share code, notes, and snippets.

@V3ntus
Created July 26, 2024 13:09
Show Gist options
  • Save V3ntus/46cc4f2a3af22ace0e9127711cb88391 to your computer and use it in GitHub Desktop.
Save V3ntus/46cc4f2a3af22ace0e9127711cb88391 to your computer and use it in GitHub Desktop.
Strawberry GraphQL dynamic query builder

What do you consider boilerplate? Is all code boilerplate? Are humans boilerplate?

Anyways, I found a stupid way to generate a whole GraphQL schema on the fly based on a dictionary of query name to source and Strawberry models. This was used in a Strawberry + FastAPI + SQLAlchemy API project.

You essentially define a mapping for your queries. Specify the query name, and which ORM or source class/model goes to the GraphQL model equivalent. The logic will then create a new Query class object, iterate over the mapping items defined above, then generate a binding with the query name, model, and GraphQL schema associated. This binding is also dynamically generated by altering the query function's signature on runtime which contains the shared, boilerplate logic between all queries.

# Centralized query bindings.
# A query entry should be the query name (string), then its value should be a tuple of the ORM or source model and the GraphQL model.
# These are used to dynamically build all necessary queries for the GraphQL router.
_query_model_bindings: Dict[str, Tuple[Type[Base], Type[BaseModelType]]] = {
"cars": (models.Car, Car),
"bikes": (models.Bike, Bike),
}
def _generate_dynamic_query_class():
def generate_bind(query_name: str, model: Base, schema: Type[BaseModelType]):
"""
Generate function instances per query by closure.
"""
def _query(
self, # noqa
info: strawberry.Info,
_id: Annotated[
int | None,
strawberry.argument(
description="The primary key item of the object desired."
)
] = None,
) -> Sequence[schema]:
"""
Baseline GraphQL query logic.
:param self: Query class instance.
:param info: The Strawberry context instance. Our custom database session is present here.
:param _id: The ID of the object to query for.
:return:
"""
if _id:
# TODO: Use your logic here. This is an example of a simple SQLAlchemy database session received from the context.
# Because we can't use union types, in order to consolidate fetch_one and fetch_all into one endpoint, make this a list with a single item.
return [info.context["db_session"].scalar(select(model).where(model.id == _id))]
else:
return info.context["db_session"].scalars(select(model)).all()
# Replace the function's name with the query name.
_query.__name__ = query_name
# Once the query function is built, return it to be assigned to the query class.
return _query
class Query:
"""
Baseline query class. Query functions will be dynamically assigned to this object.
"""
...
# Iterate over the query-to-model bindings and assign generated bindings to the Query class.
for (_query_name, (_model, _schema)) in _query_model_bindings.items():
setattr(Query, _query_name, strawberry.field(generate_bind(_query_name, _model, _schema)))
# Decorate the query class, initialize/finalize the mapper, and build the strawberry schema.
strawberry.type(Query)
strawberry_sqlalchemy_mapper.finalize()
_additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
return strawberry.Schema(
query=Query,
types=_additional_types,
)
# Generate a GraphQL schema with the dynamically built GraphQL query.
# This would then be used in the GraphQL app or router.
graphql_schema = _generate_dynamic_query_class()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment