Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoped Context #2692

Open
sammarks opened this issue Jul 6, 2020 · 6 comments
Open

Scoped Context #2692

sammarks opened this issue Jul 6, 2020 · 6 comments

Comments

@sammarks
Copy link

sammarks commented Jul 6, 2020

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.

@danielrearden
Copy link
Contributor

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).

@sammarks
Copy link
Author

sammarks commented Jul 6, 2020

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.

@glasser
Copy link
Contributor

glasser commented Aug 11, 2020

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.

@yaacovCR
Copy link
Contributor

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.

@Minishlink
Copy link

FYI graphql-ruby seems to support this too rmosolgo/graphql-ruby#2634.

@Minishlink
Copy link

Minishlink commented Nov 17, 2021

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.

ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext marker interface in your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

For now I decided to use a marker interface `ScopedContext`. Although I'm not fan of marker
interfaces, it was the easiest way to get this working. We could also make this an option that
needs to be passed to the `Executor`. Something like `$useScopedContext = false` (disabled by
default).

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext marker interface in your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

For now I decided to use a marker interface `ScopedContext`. Although I'm not fan of marker
interfaces, it was the easiest way to get this working. We could also make this an option that
needs to be passed to the `Executor`. Something like `$useScopedContext = false` (disabled by
default).

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext marker interface in your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

For now I decided to use a marker interface `ScopedContext`. Although I'm not fan of marker
interfaces, it was the easiest way to get this working. We could also make this an option that
needs to be passed to the `Executor`. Something like `$useScopedContext = false` (disabled by
default).

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext marker interface in your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

For now I decided to use a marker interface `ScopedContext`. Although I'm not fan of marker
interfaces, it was the easiest way to get this working. We could also make this an option that
needs to be passed to the `Executor`. Something like `$useScopedContext = false` (disabled by
default).

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext marker interface in your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

For now I decided to use a marker interface `ScopedContext`. Although I'm not fan of marker
interfaces, it was the easiest way to get this working. We could also make this an option that
needs to be passed to the `Executor`. Something like `$useScopedContext = false` (disabled by
default).

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext interface on your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext interface on your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext interface on your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 26, 2023
By implementing the ScopedContext interface on your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `d`
* `f` receives the cloned context from `d`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 30, 2023
By implementing the ScopedContext interface on your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `a`
* `f` receives the cloned context from `e`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
ruudk added a commit to ruudk/graphql-php that referenced this issue May 30, 2023
By implementing the ScopedContext interface on your context object, you can ensure that
scope is only passed downwards and not shared with other fields.

Example:
```graphql
query {
  a {
    b {
      c
    }
    d {
      e {
        f
      }
    }
  }
  g {
    h {
      i
    }
  }
}
```

In the above situation, the following will happen:
* `a` receives the cloned context from the executor
* `b` receives the cloned context from `a`
* `c` receives the cloned context from `b`
* `d` receives the cloned context from `a`
* `f` receives the cloned context from `e`
* `g` receives the cloned context from the executor
* `h` receives the cloned context from `g`
* `i` receives the cloned context from `h`

References:
graphql/graphql-js#2692
rmosolgo/graphql-ruby#2634
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants