-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Middleware was designed with a simple, Rack-like API, but turns out that's a bug, not a feature:
- http://blog.plataformatec.com.br/2012/06/why-your-web-framework-should-not-adopt-rack-api/
- http://tenderlovemaking.com/2011/03/03/rack-api-is-awkward.html
Similar issues affect graphql-ruby. For example, if you use graphql-batch, the actual processing is done "out of bounds", not within next_middleware.call. With DeferredExecution, the problem is even worse: there's no way to detect the actual end of the query, since any deferred fields are executed outside the root node's middleware call.
So, we need a system that:
- can intercept field resolves
- can detect the start & end of queries (even when processing is done "out of bounds", eg
graphql-batch) - supports middleware-local state (eg, timing data)
- has a user-friendly API
Whatever happens, it will be important to support existing middleware somehow. I imagine that will be simple enough, wrapping an existing middleware to port it to the new API, perhaps
legacy_middleware = CustomMiddleware.new
new_middleware = LegacyMiddlewareAdapter.new(legacy_middleware)
MySchema.middleware << new_middleware Some ideas
Make it like query analyzers
before_queryreturns an initial memo- subsequent hooks return a new memo
- problem: Previous middlewares could affect field resolution by returning a value and not calling
next_middleware.call. This implementation requires the return value to bememo, how can you intercept a field call?
MyCustomMiddleware = GraphQL::Middleware.define do
before_query -> (memo, env) {
{
counter: 0
}
}
each_field -> (memo, env) {
memo[:counter] += 1
env[:next_middleware].call
memo
}
after_query -> (memo, env) {
puts "Total fields: #{memo[:counter]}"
}
end Make it an event listener
- Execution strategy would be responsible for triggering events at the right time
- easy to add new events in the future
- problem: again, how to modify field resolutions? (I don't want to use exceptions for control flow)
MyCustomMiddleware = GraphQL::Middleware.define do
on(:begin_query) { ... }
on(:end_query) { ... }
on(:begin_field) { ... }
on(:end_field) { ... }
end Extend the current definition
- Add more hooks to middleware objects (a bit like query analyzer's
initial_valueetc) - problem: we have to keep the long list of random args
- problem: how to track out-of-bound field duration?
- problem: when we need more hooks, do we just add more methods? seems ... not elegant.
class MyCustomMiddleware
def call(*long_list_of_random_args)
# ...
end
def before_query(*more_random_args)
end
def after_query(*more_random_args)
end
end Make it like Rails instrumentation
In this API, code can be run inside an instrumentation block. Handlers can respond to incoming data in some way.
resolved_value = query.instrument("field.resolve", type_defn, field_defn) do
# call the resolve function
end
query.instrument("query.begin")
# ... do a bunch of deferred stuff
query.instrument("query.end")
batch_result = query.instrument("batch.resolve", loader) do
# Run the batch
end Some questions here:
- How are handlers identified (names, symbols) ? Is there a nesting structure? (I hope not, sounds hard)
- Can handlers interfere with the block, or only the arguments to
instrument?