Description
Description
This PR is a proposition to solve a long standing DX issue: sharing data between ALL hooks of a plugin.
Context
Today, the only relyable way to share data between the hooks of a plugin is to use WeakMap
with the GraphQL context as key:
function usePlugin(): Plugin {
new dataByContext = new WeakMap<any, { foo: string }>();
return {
onParse({ context }) {
dataByContext.set(context, { foo: 'bar' );
},
onValidate({ context }) {
const data = dataByContext(context);
if(data) { // We even have to check for its existence, even it should always be defined.
console.log(data)
}
}
}
In Yoga, it's even worse since in most Yoga specific hooks, the graphql context is not built yet, so the only thing shared by all hooks is the HTTP request:
function usePlugin(): YogaPlugin {
new dataByRequest = new WeakMap<Request, { foo: string }>();
return {
onRequestParse({ request }) {
dataByRequest.set(request, { foo: 'bar' );
},
onValidate({ context }) {
const data = dataByRequest(context.request);
if(data) { // We even have to check for its existence, even it should always be defined.
console.log(data)
}
}
}
But this can be misleading: If batching is enabled, multiple different operation will share the same data. We currently have multiple plugins that are using this technique and that are open to bugs when batching is enabled.
Proposition
My proposal is to add a dedicated API directly in the core of Envelop, Yoga and Hive to have a streamlined way of sharing data between hooks. While the WeakMap
trick works, it's really not very good DX, and is very error prone.
The API would be a data
field that will be present in ALL hooks (for Envelop hooks, but also Yoga and Hive hooks).
To emphases the fact that there is potentially multiple level of data sharing (operation wide, http request wide, subgraph execution wide), this data
object would contain a field for each scope:
type EnvelopData = {
forOperation: object
}
type YogaData = EnvelopData & {
forRequest: object
}
type HiveData = YogaData & {
forSubgraphExecution: object
}
This data
would then be available in all hooks, but depending of the hook, not all scopes will be available.
function myPlugin(): HivePlugin {
return {
onRequestParse({ data }) {
// only data.forRequest is available
},
onExecute({ data }) {
// both data.forRequest and data.forOperation are available
},
onSubgraphExecute({ data }) {
// all scopes are available: data.forRequest, data.forOperation and data.forSubgraphExecution
},
}
Of course, it will also imply adding generic types everywhere. And this is probably a bit tricky because it will imply adding more generics to existing Plugin type, which I'm not sure if it is non-breaking or not.
As a proof of concept, I've implemented the first step of this proposal: the Envelop scope (#2405)