-
Notifications
You must be signed in to change notification settings - Fork 2k
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
RFC: Assert subscription field is not introspection. #2861
Conversation
15eded0
to
ba1a314
Compare
@@ -6,9 +6,10 @@ import type { OperationDefinitionNode } from '../../language/ast'; | |||
import type { ASTValidationContext } from '../ValidationContext'; | |||
|
|||
/** | |||
* Subscriptions must only include one field. | |||
* Subscriptions must only include a non-introspection field. |
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.
This is very precise wording; if we were to use one non-introspection field
or a single non-introspection field
here then it wouldn't place limits on the introspection fields we added, only the non-introspection fields.
ba1a314
to
afe10e7
Compare
I've updated the PR to overhaul the subscription validation rule to walk fragments too (see #2715). |
@benjie You need to handle recursive fragments: subscription {
...A
}
fragment A {
...A
} Even thought it's invalid, validation shouldn't be stuck on it |
@IvanGoncharov I've handled that by tracking |
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.
Tests look great 👍
Implementation is also looking correct.
I just like to play with the source code of the rule to see if it can be simplified.
Will try to do this on weekends.
*/ | ||
export function SingleFieldSubscriptionsRule( | ||
context: ASTValidationContext, | ||
): ASTVisitor { | ||
return { | ||
OperationDefinition(node: OperationDefinitionNode) { | ||
if (node.operation === 'subscription') { | ||
if (node.selectionSet.selections.length !== 1) { |
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.
Happy to also see this validation bug being fixed - fields are collected before asserting a single selection
* Walks the selection set and returns a list of selections where extra fields | ||
* were selected, and selections where introspection fields were selected. | ||
*/ | ||
function walkSubscriptionSelectionSet( |
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.
Any reason why this can't be a more typical CollectFields()
implementation? I'm surprised there isn't something to be reused from another validation rule.
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 closest I could find was _collectFieldsAndFragmentNames
from src/validation/rules/OverlappingFieldsCanBeMergedRule.js
but it doesn't recurse into named fragments. We also can't use collectFields from execution because it requires an execution context.
My algorithm is more efficient than recursing over calls to _collectFieldsAndFragmentNames
because it has no need to reference the underlying types - there's no need for calls to parentType.getFields()
/typeFromAST
/isObjectType
/etc since all we want to know is:
- is there any
__introspection
fields? - is there more than one field?
The actual type they're defined on (or even whether it exists or not!) is irrelevant.
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.
I suppose that could potentially be untrue; e.g.
type Query implements Node {...}
type Subscription implements Node {...}
subscription ShouldBeFine($bool: Boolean!) {
... on Node {
... on Query { __typename id firstName }
... on Subscription {
mySubscriptionField @include(if: $bool)
myOtherSubscriptionField @skip(if: $bool)
}
}
}
in this case a runtime collectFields
would return only mySubscriptionField
or myOtherSubscription
field (depending on the value of $bool
) because the Query selections would be scoped out as incompatible types and the @include
/@skip
would ensure only one field was valid. This is problematic though, since in validate we don't have access to the runtime variables so we cannot determine which of the fields will/won't be included.
In the spec edit I wrote this leveraging the CollectFields
call which was already present, which already relies on varibleValues
; but GraphQL.js allows validation without variables so... is this a fundamental issue in the way GraphQL.js is built? Personally I think we should edit the spec to reflect GraphQL.js' behaviour - validating the query without access to the variables seems desirable and so having the validation rule for "single root field" depend on CollectFields which depends on variableValues is undesirable.
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.
Reading more carefully the spec explicitly makes variableValues an empty object, so in this case the operation would be valid; however so would the following:
type Query implements Node {...}
type Subscription implements Node {...}
subscription ShouldBeFine($bool: Boolean!) {
... on Node {
... on Query { __typename id firstName }
... on Subscription {
mySubscriptionField1 @include(if: $bool)
mySubscriptionField2 @include(if: $bool)
mySubscriptionField3 @skip(if: $bool)
}
}
}
In this case the CollectFields algorithm would result in only mySubscriptionField3
being considered; the operation would pass validation, but then if $bool
was set to true
at runtime there'd be two selections and the operation would not work (it would raise a request error
in step 5 of CreateSourceEventStream
).
This is definitely a spec bug. I'm going to write up a proper fix.
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.
(After more thought, I'm not sure it's a bug so much as an "unexpected solution"; but I've filed a potential spec edit anyway: graphql/graphql-spec#860)
Given the above discussion I've rewritten the solution to use CollectFields so that it can take into account the fragment compatibility and the @include
/@skip
directives in the same way that the GraphQL spec would.
To address @leebyron's feedback I've rewritten the implementation to more closely follow the spec - it calls into This is the only place in Section 5 of the GraphQL spec that utilises |
8177dbf
to
9f718b8
Compare
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.
Generally looks good 👍
Not really happy with the fact that we create fake execution context but it's the only way to use collectFields
at the moment.
Added FIXME to show that we plan to change that in the future since collectFields
doesn't really need the whole execution context.
This PR implements the spec changes in the following GraphQL Spec RFC:
graphql/graphql-spec#776