graphql-js icon indicating copy to clipboard operation
graphql-js copied to clipboard

Scoped Context

Open sammarks opened this issue 3 years ago • 6 comments

Apologies if this is meant to go with the specification instead of this specific client, but I couldn't find much reference to the concept of context inside the official specification, so assumed it was a client-specific implementation detail.

With that said, a .NET GraphQL client has the following feature: Scoped Context Data

I've seen a few issues revolving around this idea (this one in particular comes closest), but I haven't seen any issues call out the functionality explicitly.

I think the documentation from Hot Chocolate explains it best:

Scoped state allows us to aggregate state for our child field resolvers.

Let's say we have the following query:

{
  a {
    b {
      c
    }
  }
  d {
    e {
      f
    }
  }
}

If the a-resolver would put something on the scoped context its sub-tree could access that data. This means, b and c could access the data but d, e and f would NOT be able to access the data, their dictionary is still unchanged.

Since I'm familiar with React, I also like to think of it as React.Context, but applied to GraphQL.

Arguably, I would assume the general suggestion for this kind of problem would be something along the lines of "adjust your architecture," but I still do believe the feature has some benefit once you get into some advanced schemas.

Another Use-Case

If you would like another use-case, the one I am running into specifically goes as follows:

  • I have a list of Comment types, each containing the user that posted the comment.
  • This list of Comment types lives on a Media type, which represents a general piece of media and is used in several places throughout my service.
  • In another part of the codebase, we must enforce that user names are not displayed in comments on "child" media nodes. I would much rather enforce this on the server-side instead of doing it on the frontend, as they can still see the names by inspecting the request.

The solution, in this case, is to have the parent resolver set a flag in the context that causes the Comment resolver way down stream to hide the associated user.

sammarks avatar Jul 06 '20 20:07 sammarks

I can see the value in adding this as a feature, although it'd need to be configurable. Scoping context this way would require cloning the context object repeatedly, which would result in additional overhead. It would also prevent being able to pass down some object that'd be mutated during execution, like a Response object from an HTTP server (although admittedly that sort of things should only be done at the root level anyway).

danielrearden avatar Jul 06 '20 21:07 danielrearden

In order to decouple it from the main context object, without knowing the inner workings of the client, my initial thought was to create some sort of object where each change to the context would be additive, and then the resulting “scoped context” object inside the children resolvers would just be a merged combination of the contexts set by all of the parent resolvers (in a quick and dirty middleware approach I would use the path field in the resolver info to identify the ancestry).

It might be worth considering separating the scoped context away from the context as it is today completely in order to avoid issues you described where mutations to the context need to be shared globally across all resolvers, and so cloning wouldn’t have to be done (causing the performance overhead).

I’m planning on making a prototype middleware (which might be sufficient as your on-off switch, just include the middleware) in the next few weeks, that just injects this “scoped context manager” inside the main context object that does exactly as I described above.

Individual contributions would be tracked with their path and the resulting (additive) context would be built for each resolver requesting it by looking at the path of the current resolver and looking for entries to the scoped context with paths that indicate ancestry to the current resolver.

sammarks avatar Jul 06 '20 21:07 sammarks

GraphQL-Java has this feature, which they call local context. https://www.graphql-java.com/blog/deep-dive-data-fetcher-results/ is an explanation of how it works.

Basically it's a mechanism that allows you to use your application's model types directly for the "parent" objects rather than having to encode contextual data in the "parent" objects.

glasser avatar Aug 11 '20 21:08 glasser

In GraphQL Java you can use well known graphql.execution.DataFetcherResult to return three sets of values

data - which will be used as the source on the next set of sub fields errors - allowing you to return data as well as errors localContext - which allows you to pass down field specific context

The errors argument would probably simplify schema proxying/stitching as well.

yaacovCR avatar Aug 11 '20 22:08 yaacovCR

FYI graphql-ruby seems to support this too https://github.com/rmosolgo/graphql-ruby/pull/2634.

Minishlink avatar Nov 17 '21 16:11 Minishlink

As a "simple" workaround you can do this by using a map in the context where each key represents a specific entity (eg TYPENAME-ID) and the value is the "scoped context". Limitations include: you need to know the entity's id's key (eg. id/ref etc), and if an entity is present outside of your current branch in the tree, it will still have the scoped context.

Minishlink avatar Nov 17 '21 18:11 Minishlink