You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Wrapping the execution of GraphQL operations is a very common need. For example, it is needed for tracing, monitoring, error handling, cache, etc...
Today, this is doable with the setExecuteFn API, which allow to entirely replace the execute function. By keeping a reference to the previous one, it is possible to wrap the execution phase:
constplugin: Plugin={onExecute({ setExecuteFn, executeFn }){setExecuteFn(async(...args)=>{// The plugin can do things just before the execution beginsconstresult=awaitexecuteFn(...args)// And do things just after the execution has ended.returnresult})}}```But this is fragile, since this API is very permissive and gives a lot of control over the execution phase.The most problematic side effect is that if a plugin replace the execution function without wrapping the existing one, it will break any subsequent plugin also relying on wrapping the execution functon.```tsconstwrappingPlugin: Plugin={onExecute({ setExecuteFn, executeFn }){setExecuteFn(async(...args)=>{console.time('execute')constresult=awaitexecuteFn(...args)console.time('execute')returnresult})}}constreplacingPlugin: Plugin={onExecute({ setExecuteFn, executeFn }){setExecuteFn(myFancyCustomExecutor)}}constyoga=createYoga({plugins: [wrappingPlugin,replacingPlugin,],})
In this example, one would expect both the having a custom executor and the execution time log in the console.
Actually, this is not working, because the replacingPlugin just overrides the wrapped execution function set by wrappingPlugin.
To make it work, the plugin order should be reversed:
The plugins are order dependent, and the order can be know only by understanding the internal implementation details of the plugin, which is really not a good DX, especially for newcomers.
Solution proposal
A solution would be to have a dedicated hook for wrapping the execution phase. This would allow to create some kind of middleware.
To avoid having the same issues than setExecuteFn have, this API should enforce the fact that it can't cutoff the execution chain. All plugins should always be called for every execution. Otherwise, the point of having a new API would be purely a sort of "documentation", relying on the good will of plugin's maintainers to respect the rule.
To enforce this, the responsibility of actually calling the execution function and all the plugin wrapping hooks should remain inside Envelop core.
To achieve that, we could rely on an execution Promise instead of an execution Function. The wrapper will have to await for a promise instead of directly calling the execution function. This way, if the wrapper doesn't await the given Promise, it's a bug in this plugin and it will not break other plugins in the chain.
Here an example of the previous plugin with this new API:
constplugin: Plugin={onExecuteWrap(({ executed }){console.time('execute')awaitexecuted// We wait for the execution phase to be done, instead of executing it directlyconsole.time('executed')}}
It would be possible to have access to potential errors:
If needed, we can even give access to executionArgs and result:
constplugin: Plugin={onExecuteWrap(({ executionArgs, executed }){const result =awaitexecutedconsole.time('executed',executionArgs,result)}}
Perhaps we could even offer a way to set the result, but I'm not sure about this one.
Alternatives
An alternative would be to offer a classic middleware API like this:
constplugin: Plugin={onExecuteMiddleware({ executionArgs, next }){constresult=awaitnext(executionArgs)returnresult}}
But the problem with this approach is that nothing prevents the plugin to not call the next function, lead to the exact same problem we currently have with setExecuteFn. The main difference is that since this API is dedicated to wrapping, it would be considered a bug if a plugin breaks the expectation of calling the next function. But compared to the previous proposition, this bug would break the entires plugin chain.
Additional context
This is in the context of multiple users having issues writing reliable tracing or monitoring plugins, such as the OTEL plugin.
The text was updated successfully, but these errors were encountered:
The problem
Wrapping the execution of GraphQL operations is a very common need. For example, it is needed for tracing, monitoring, error handling, cache, etc...
Today, this is doable with the
setExecuteFn
API, which allow to entirely replace the execute function. By keeping a reference to the previous one, it is possible to wrap the execution phase:In this example, one would expect both the having a custom executor and the execution time log in the console.
Actually, this is not working, because the
replacingPlugin
just overrides the wrapped execution function set bywrappingPlugin
.To make it work, the plugin order should be reversed:
The plugins are order dependent, and the order can be know only by understanding the internal implementation details of the plugin, which is really not a good DX, especially for newcomers.
Solution proposal
A solution would be to have a dedicated hook for wrapping the execution phase. This would allow to create some kind of middleware.
To avoid having the same issues than
setExecuteFn
have, this API should enforce the fact that it can't cutoff the execution chain. All plugins should always be called for every execution. Otherwise, the point of having a new API would be purely a sort of "documentation", relying on the good will of plugin's maintainers to respect the rule.To enforce this, the responsibility of actually calling the execution function and all the plugin wrapping hooks should remain inside Envelop core.
To achieve that, we could rely on an execution Promise instead of an execution Function. The wrapper will have to await for a promise instead of directly calling the execution function. This way, if the wrapper doesn't await the given Promise, it's a bug in this plugin and it will not break other plugins in the chain.
Here an example of the previous plugin with this new API:
It would be possible to have access to potential errors:
If needed, we can even give access to executionArgs and result:
Perhaps we could even offer a way to set the result, but I'm not sure about this one.
Alternatives
An alternative would be to offer a classic middleware API like this:
But the problem with this approach is that nothing prevents the plugin to not call the
next
function, lead to the exact same problem we currently have withsetExecuteFn
. The main difference is that since this API is dedicated to wrapping, it would be considered a bug if a plugin breaks the expectation of calling thenext
function. But compared to the previous proposition, this bug would break the entires plugin chain.Additional context
This is in the context of multiple users having issues writing reliable tracing or monitoring plugins, such as the OTEL plugin.
The text was updated successfully, but these errors were encountered: