strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations πŸ“
https://strawberry.rocks
MIT License
4.01k stars 533 forks source link

Support dependency injection in resolvers #2413

Open german-glob opened 1 year ago

german-glob commented 1 year ago

Support dependency injection in resolvers

Feature Request Type

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

Fund with Polar

mayteio commented 1 year ago

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

jgadling commented 1 year ago

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

patrick91 commented 1 year ago

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

jgadling commented 1 year ago

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

patrick91 commented 1 year ago

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!

mayteio commented 1 year ago

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.

jgadling commented 1 year ago

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

mayteio commented 1 year ago

Thanks @jgadling 😎

patrick91 commented 1 year ago

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)

erikwrede commented 1 year ago

@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. πŸš€πŸš€

skilkis commented 1 year ago

@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

aurthurm commented 1 year ago

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

erikwrede commented 1 year ago

@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