add support for general purpose middleware #1890
Replies: 2 comments 9 replies
-
I was the one asking the original question on discord. As I see it, from strawberry.middleware import BaseMiddleware, MiddlewareResponse
@strawberry.type
class Result:
message: str
@strawberry.type
class PossibleMiddlewareResult:
something: str
@strawberry.type
class WrongPasswordError:
details: str = 'Wrong password was provided'
@strawberry.input
class Input:
password: str
...
MyQueryResult = strawberry.union(
'MyQueryResult', (Result, PossibleMiddlewareResult, WrongPasswordError)
)
class MyMiddleware(BaseMiddleware):
def run(self, info: Info, input: Input) -> MiddlewareResponse[PossibleMiddlewareResult, WrongPasswordError]:
user = info.context['user']
if is_lucky_user(user):
return PossibleMiddlewareResultt(something="foo") # this will get returned no other middleware will run
elif user.password != input.password:
return WrongPasswordError("wrong password") # an exception will get returned
return # nothing happens, next middleware or field will run normally
@strawberry.type
class Query
@strawberry.field(middleware=[MyMiddleware])
async def my_field(self, input: Input) -> MyQueryResult: ... Here I really like idea of being able to chain middlewares together. However, I'm not sure on how user will define such chains. Using just a list seems a bit dirty to me. But maybe that's me overthinking it because I can't find any real reason to why that would be inconvenient. Also I've replaced raising Overall, middlewares just seem like "better permissions" with some useful features, so in my opinion they would be quite a useful addition. |
Beta Was this translation helpful? Give feedback.
-
So I thought about this some more. I think it would be nice if the response of such a middleware wouldn't be strictly tied to a single response type. Instead implement it as a dict internally, and allow any return types that will allow Here is some code I came up with. from typing import TypeVar
from typing import Generic
from typing import Type
import strawberry
K = TypeVar('K')
# the generic type indicates to what type
# we can coerce the response to be in _get_resp
class MiddlewareResponse(Generic[K]):
def __init__(self, **kwargs):
self.vars = {k: v for k, v in kwargs.items()}
def _get_resp(self, cls: Type[K]) -> K:
return cls(**self.vars)
T = TypeVar('T')
class BaseMiddleware(Generic[T]):
_ret_cls: Type[T]
def __init__(self, return_cls: Type[T]) -> None:
self._ret_cls = return_cls
# this method takes **kwargs so you are flexible with
# how you want to use args
# however it should return responses that can be coerced
# to be of type T, as explained above
def run(self, **kwargs) -> MiddlewareResponse[T] | None:
raise NotImplementedError()
class MyMiddleware(BaseMiddleware):
def run(self, **kwargs) -> MiddlewareResponse | None:
if True:
return MiddlewareResponse(foo='bar') # return early
elif False:
raise Exception('foo') # raise exception
else:
return None # do nothing, run next
# this logic would live in strawberry.field, ...
def middleware_runner(input, field, middleware_list: list[BaseMiddleware]):
for middleware in middleware_list:
try:
mw_resp = middleware.run()
except Exception as e: # perhaps logging?
raise
if isinstance(mw_resp, MiddlewareResponse):
return mw_resp._get_resp(middleware._ret_cls)
return field(input)
@strawberry.input
class Input:
...
@strawberry.field
class Response:
foo: str = 'foo'
bar: str = 'bar'
# we would now define middleware instances like so
mw_list = [MyMiddleware(Response)] Im not sure if mypy will understand that as for what you said about Exceptions. Yes I agree you probably shouldn't raise exceptions in middleware, its better to return graphql error types. But that's a larger question for strawberry, how to handle errors. |
Beta Was this translation helpful? Give feedback.
-
so a question was asked in discord about how to best reuse logic between fields (or queries and mutations). If the logic doesn't depend on arguments passed to the field we would use permission classes for this.
However if the logic depends on the inputs to that field there is currently, to my knowledge, no better way, than to outsource that logic into a function and call it at the beginning of each field. So i though about this and came up with this idea:
suppose we allowed an argument such as perhaps
middleware
tostrawberry.field
, which would be a list of functions, that get executed (in order), on the input. Forwarding the input (to the next middleware function or the field itself) if everything is fine, perhaps raising exceptions or even returning a response early. Importantly the input arguments should not be mutated as to allow for a predictable execution.Perhaps a user would be able to define middleware by inheriting from some middleware class, where the user would have to implement a specific class method.
run
would have to be able to handle all arguments provided tomy_field
, which i think should be fine. But at the moment I mainly see the problem that the middleware has to return a specificResult
type.What are your thoughts on this? would this be a useful feature? Is there already a better way to do this?
Beta Was this translation helpful? Give feedback.
All reactions