-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Adds scoped context for propagating values specific to a field and its children #2634
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
Conversation
@@ -155,6 +155,7 @@ def initialize(query:, values: , object:) | |||
@path = [] | |||
@value = nil | |||
@context = self # for SharedMethods | |||
@scoped_context = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial implementation relied on Context@scoped_context
being an ImmutableHash
instead of an Hash
. I ended up opting out of that decision since it provides little benefit unless we can sure that all values, no matter how deeply nested, are immutable. That being said, with @scoped_context
being a Hash
, there's nothing stopping an arbitrary resolver from keeping a reference to an object which is used in a separate scoped context, and modifying it from there. I think that's okay though, since doing so would invalidate the goal of keeping this reference only for a field and its children.
👍 Thanks for taking a crack at this! Nice to see how little overhead is required. Thanks for the comprehensive tests, too. One general question I have about the design is, should we require uses to distinguish between scoped context values (using I guess what I'm getting at is, what if |
Having scoped values be fetcheable from the existing methods Hash-like methods could work! I'm assuming that if a scoped value is set, it should always take precendence over the unscoped value. In that case, we'd have to be okay with always showing the scoped value if it is set, even if the unscoped value was set more recently. Eg. context.scoped_merge!(value: 'old')
context[:value] = 'new'
assert_equal('old', context[:value])
assert_equal({ value: 'old' }, context.to_h)
# later, in sibling resolver ...
assert_equal('new', context[:value])
assert_equal({ value: 'new' }, context.to_h) Otherwise, the implementation would need to keep track of the order in which scoped vs. unscoped values were inserted, or at least which one was most recently inserted, which also works. |
👍 |
…ropagating values specific to a field and its children
With the new API for getters, this is ready for a round of review 😄 |
|
||
# @!method [](key) | ||
# Lookup `key` from the hash passed to {Schema#execute} as `context:` | ||
def_delegators :@provided_values, :[]= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 is it inconsistent that the getter []
will magically return scoped values but the setter []=
won't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API could be a little confusing if the developer isn't aware of the existence of #scoped_merge!
. Using a combination of #[]=
and #scoped_merge
can also lead to misleading behaviour with #[]
because it always retrieves the value from @scoped_context
if it exists regardless of the existing value in @provided_values
, as described in #2634 (comment)
context.scoped_merge!(value: 'old')
context[:value] = 'new'
assert_equal('old', context[:value])
I can definitely see how this usage of the API can be misleading. I could also see this more as an edge case though. This only arises when values are set in ☝️ configuration, and using scoped_merge!(special_key: 1)
in combination with [:special_key] = 2
is probably not what that developer means to do, because by doing [:special_key] = 2
, they are voiding the benefit of having :special_key
be scoped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think what you have makes sense 👍
Also wondering if set_scoped(key, value)
should exist in addition to scoped_merge!
as a convenience. If a common use case is setting a single key then it might match people's normal usage of setting a hash key?
Meaning most people would use []=
instead of merge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! I like set_scoped(key, value)
as a convenience. I'll push a commit shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added Context#scoped_set!(key, value)
in commit afc292a.
🎉 Thanks for all your work exploring and implementing this! I'm going to follow up on some old issues asking for this feature. |
Would it be possible to add/update docs with how to use this? Looks like a useful feature! |
As the previous comment also stated, would it be possible to get some documentation on how to get this to work? I need this feature right now for something we are working on and I have tried to use |
I just got it to work, but still, having documentation would be useful :) Thanks! |
There's documentation at https://graphql-ruby.org/queries/executing_queries.html#scoped-context, please open a new issue (or submit a PR) if you think it can be improved! |
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
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
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
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
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
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
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
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
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
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
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
Resolves #2602.
This PR adds a "scoped context" to the existing context. This is useful for propagating values which are specific to a field and its children, as opposed to for the entirety of the query.
This is achieved with very little overhead on the interpreter runtime by simply reinitializing the scoped context at various points where new resolvers would run within a possibly new context as the query is being executed.