ciscorn / starlette-graphene3

An ASGI app for using Graphene v3 with Starlette / FastAPI
MIT License
101 stars 14 forks source link

Is there a Recommended Way of Handling Dependency Injections? #24

Open Stuwert opened 2 years ago

Stuwert commented 2 years ago

I'm trying to understand the best way to inject the database as a dependency for my graphql queries/mutations.

It looks like context is the appropriate way to do so, and based on reviewing other issues raised on the starlette repo https://github.com/encode/starlette/issues/591, it sounds like the problem was never really solved. Given that starlette is deprecating graphql support and this repo appears to be filling in the gaps of that connection, it felt right to ask the question here.

The workaround described in issue 591 mentions adding the db session to the request via injection on the route, however, that appears to require calling execute on the GraphQL app directly, rather than the app.mount() pattern described in ythe README. I wasn't able to find another way attach the GraphQL app to appropriately, so I'm wondering if I'm missing something or if db dependency injection just isn't possible at the moment.

Is this functionality currently possible to manage or is the expectation that the db will be accessed via things like the SQLAlchemy Graphene connection?

rachthree commented 1 year ago

Not sure if this is what you're looking for, but I tried two different ways that retain the app.mount() pattern: 1) Provide a callable to context_value (this appears to be the intent looking at https://github.com/ciscorn/starlette-graphene3/blob/0.6.0/starlette_graphene3.py#L144 and https://github.com/ciscorn/starlette-graphene3/blob/0.6.0/tests/conftest.py#L98):

from starlette.background import BackgroundTasks
from fastapi import FastAPI

class Context(object):
    def __init__(self, foo_resource):
        self.foo_resource= foo_resource

    async def __call__(self, request):
        return {
            "foo_resource": self.foo_resource,
            "request": request,   # provided to maintain previous default behavior
            "background": BackgroundTasks(),  # provided to maintain previous default behavior
        }

foo_resource = get_foo()
graphql_app = GraphQLApp(schema=schema, context_value=Context(foo_resource))

app = FastAPI()
app.mount("/graphql", graphql_app)

2) Subclass GraphQLApp and overload _get_context_value:

from fastapi import FastAPI

class AugmentedGraphQLApp(GraphQLApp):
    def __init__(self, *args, **kwargs):
        foo_resource = kwargs.pop("foo_resource")
        super().__init__(*args, **kwargs)

        self.foo_resource = foo_resource 

    async def _get_context_value(self, request):
        context = await super()._get_context_value(request)
        context["foo_resource"] = self.foo_resource
        return context

foo_resource = get_foo()
graphql_app = AugmentedGraphQLApp(schema=schema, foo_resource=foo_resource)

app = FastAPI()
app.mount("/graphql", graphql_app)

I was then able to use context["foo_resource"] using both ways. Disclaimer: I was using graphene-sqlalchemy, so in actuality I had to use info.context["foo_resource"]. I know you were trying to avoid using the SQLAlchemy Graphene connection but I had to use another client (my foo_resource contacting another server) on top of using graphene-sqlalchemy in my project. I would think having the DB session as foo_resource would work.

Edit: missing lines of code