strawberry icon indicating copy to clipboard operation
strawberry copied to clipboard

Support dependency injection in resolvers

Open german-glob opened this issue 2 years ago • 13 comments

Support dependency injection in resolvers

Feature Request Type

  • [ ] Core functionality
  • [x] Alteration (enhancement/optimization) of existing feature(s)
  • [ ] New behavior

Description

In my use case, I have a public GraphQL API and private FastAPI API, both running on the same webserver and sharing some internal logic, so I'd like to be able to reuse my dependencies between them.

Ideally, I'd like to do something like this:

from fastapi import Depends

def some_resolver(
    session_maker = Depends(session_maker),
    store = Depends(store)
):
    async with session_maker() as session:
        return store.search(session=session)

or this:

from dependency_injector.wiring import Provide, inject

@inject
def some_resolver(
    session_maker = Provide[Container.session_maker],
    store = Provide[Container.store]
):
    async with session_maker() as session:
        return store.search(session=session)

Currently, when trying any of those dependency injection methods, graphql/type/definition.py raises TypeError: Query fields cannot be resolved. Unexpected type....

I know I can pass my custom dependencies using GraphQLRouter's context_getter param, but that solution sacrifices typehinting.

Is it feasible for Strawberry to stop treating function params as graph query params when they are not strawberry types? Or approaching it from other direction, is it possible to introduce a way to 'mark' some params, so they are not treated as query params?

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.
Fund with Polar

german-glob avatar Dec 22 '22 14:12 german-glob

+1, this would be awesome! Especially with dependency_injector.

mayteio avatar Jul 05 '23 21:07 mayteio

I have a rough demo of using Strawberry's FieldExtension to do this. It uses FastAPI internals though, so ymmv if they do a refactor: https://gist.github.com/jgadling/1971426b0075073ea6d13d64cade1310

jgadling avatar Jul 27 '23 20:07 jgadling

@jgadling that's pretty cool! would be interested in having this into Strawberry?

patrick91 avatar Jul 27 '23 21:07 patrick91

@patrick91 absolutely, if you think it would be a useful addition!

jgadling avatar Jul 27 '23 21:07 jgadling

judging from the reaction on this issue I think it is!

I guess ideally we could have a strawberry.fastapi.field in future, but I think just having the extension for now it is fine 😊

also /cc @erikwrede, I'm sure you'd like to see this!

patrick91 avatar Jul 27 '23 22:07 patrick91

I guess ideally we could have a strawberry.fastapi.field in future, but I think just having the extension for now it is fine 😊

Ideally not FastAPI specific! If there's a way to mark arguments in general as not inputs/fields (as in the first comment) that would be amazing. This means we could choose the dependency injection method.

mayteio avatar Jul 27 '23 22:07 mayteio

A dependency_injector flavor: https://gist.github.com/jgadling/c23eebf4a08c8db199df2a4fd70bf555

jgadling avatar Jul 28 '23 01:07 jgadling

Thanks @jgadling 😎

mayteio avatar Jul 28 '23 04:07 mayteio

Ideally not FastAPI specific! If there's a way to mark arguments in general as not inputs/fields (as in the first comment) that would be amazing. This means we could choose the dependency injection method.

I think we can add support for strawberry.Private on fields 😊 would that work for you? (it would just hide the field from GraphQL)

patrick91 avatar Jul 28 '23 09:07 patrick91

@jgadling this is awesome! So amazing to see all the great use cases of FieldExtension come to life so quickly 😊

I agree with @mayteio, the field should not be FastAPI-Specific, but it makes sense to have a specific DI-Extension for FastAPI for a PoC. Later on I could also imagine some strawberry.Inject which maps to several injection backends during schema creation - e.g. the FastAPI one, or maybe even a custom strawberry one. /cc @patrick91

I also thought about automatically applying the DI-Injection extension in schema creation, but this has several problems:

  1. which extension should we choose - we want to keep the schema generation as abstract as possible.
  2. In which order should the extensions be applied? The DI-Extension should always be the innermost extensions.

Summing up, I think refining the FastAPI Extension a bit is a great start for an initial PoC for DI, maybe with an experimental note so we can feel safe about revamping the design later. 🚀🚀

erikwrede avatar Jul 28 '23 10:07 erikwrede

@jgadling this is awesome! So amazing to see all the great use cases of FieldExtension come to life so quickly 😊

I agree with @mayteio, the field should not be FastAPI-Specific, but it makes sense to have a specific DI-Extension for FastAPI for a PoC. Later on I could also imagine some strawberry.Inject which maps to several injection backends during schema creation - e.g. the FastAPI one, or maybe even a custom strawberry one. /cc @patrick91

I also thought about automatically applying the DI-Injection extension in schema creation, but this has several problems:

  1. which extension should we choose - we want to keep the schema generation as abstract as possible.
  2. In which order should the extensions be applied? The DI-Extension should always be the innermost extensions.

Summing up, I think refining the FastAPI Extension a bit is a great start for an initial PoC for DI, maybe with an experimental note so we can feel safe about revamping the design later. 🚀🚀

  1. It could be an approach to let the user decide which FieldExtension to apply to any field by providing "filter" callables. Essentially making it a higher order function similar to Python's filter. Psuedo-code could be something like:
ExtensionRule = Callable[[StawberryField, StawberryType], bool]
extensions: Dict[FieldExtension, ExtensionRule]

for field in type:
     for extension, rule in extensions.items():
          if rule(field, type):
               field.extensions.append(extension)
  1. We could let the user indicate a "priority" similar to how Sphinx documenters work, alternatively the simple dict above provided by the user would suffice IMO where the order in the dict should be the order in which extensions should be applied. This is symmetric to how schema extensions work right now 🍓

edit: I'm on my phone right now so sorry for the formatting

skilkis avatar Jul 29 '23 07:07 skilkis

Hie @jgadling

Thank yu for this awesome extension. I was wndering if there is a way for making it global like this schema = strawberry.Schema(query=Query, extensions=[DependencyExtension()])

Thanks

aurthurm avatar Oct 22 '23 11:10 aurthurm

@aurthurm We currently have no decided standard to apply a field extensions to all schema fields where applicable. Check out mine and @skilkis comment above where we discuss the idea

erikwrede avatar Oct 22 '23 13:10 erikwrede