From 5e742bbdc525f146399ad1f88566f270d440086d Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Sun, 25 Apr 2021 12:00:11 +0100 Subject: [PATCH 1/3] Prevent @skip and @include on root subscription selection set --- spec/Section 5 -- Validation.md | 32 ++++++++++++++++++++++++-------- spec/Section 6 -- Execution.md | 23 +++++++++++++++++------ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index c33157705..724abc7f2 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -249,19 +249,22 @@ query getName { **Formal Specification** -* For each subscription operation definition {subscription} in the document -* Let {subscriptionType} be the root Subscription type in {schema}. -* Let {selectionSet} be the top level selection set on {subscription}. -* Let {variableValues} be the empty set. -* Let {groupedFieldSet} be the result of - {CollectFields(subscriptionType, selectionSet, variableValues)}. -* {groupedFieldSet} must have exactly one entry, which must not be an - introspection field. +* For each subscription operation definition {subscription} in the document: + * Let {subscriptionType} be the root Subscription type in {schema}. + * Let {selectionSet} be the top level selection set on {subscription}. + * Let {groupedFieldSet} be the result of + {CollectFields(subscriptionType, selectionSet, null)}. + * {groupedFieldSet} must have exactly one entry, which must not be an + introspection field. **Explanatory Text** Subscription operations must have exactly one root field. +To enable us to determine this without access to runtime variables, we must +forbid the `@skip` and `@include` directives in the root selection set (by +passing `null` as the `variableValues` argument to {CollectFields()}). + Valid examples: ```graphql example @@ -312,6 +315,19 @@ fragment multipleSubscriptions on Subscription { } ``` +We do not allow the `@skip` and `@include` directives at the root of the +subscription operation. The following example is also invalid: + +```graphql counter-example +subscription requiredRuntimeValidation($bool: Boolean!) { + newMessage @include(if: $bool) { + body + sender + } + disallowedSecondRootField @skip(if: $bool) +} +``` + The root field of a subscription operation must not be an introspection field. The following example is also invalid: diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ace837f2e..ed62666e2 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -479,17 +479,28 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. +When {CollectFields()} is used during validation (see for example the +[single root field](#sec-Single-root-field) subscription operation validation +rule), the runtime value for {variableValues} will not be available - in this +case we set {variableValues} to {null} and forbid the use of the `@skip` and +`@include` directives. During execution, {variableValues} will always be +non-null. + CollectFields(objectType, selectionSet, variableValues, visitedFragments): * If {visitedFragments} is not provided, initialize it to the empty set. * Initialize {groupedFields} to an empty ordered map of lists. * For each {selection} in {selectionSet}: - * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. - * If {skipDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. - * If {selection} provides the directive `@include`, let {includeDirective} be that directive. - * If {includeDirective}'s {if} argument is not {true} and is not a variable in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. + * If {variableValues} is {null}: + * {selection} must not provide the `@skip` directive. + * {selection} must not provide the `@include` directive. + * Otherwise: + * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. + * If {skipDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}, continue with the next + {selection} in {selectionSet}. + * If {selection} provides the directive `@include`, let {includeDirective} be that directive. + * If {includeDirective}'s {if} argument is not {true} and is not a variable in {variableValues} with the value {true}, continue with the next + {selection} in {selectionSet}. * If {selection} is a {Field}: * Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). * Let {groupForResponseKey} be the list in {groupedFields} for From 0308b27f1de82201b94e45ef83b5c18a4408a38a Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 May 2021 16:12:28 +0100 Subject: [PATCH 2/3] Use a modified copy of the CollectFields algorithm --- spec/Section 5 -- Validation.md | 54 +++++++++++++++++++++++++++++++-- spec/Section 6 -- Execution.md | 23 ++++---------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 724abc7f2..1d82b5c79 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -253,17 +253,65 @@ query getName { * Let {subscriptionType} be the root Subscription type in {schema}. * Let {selectionSet} be the top level selection set on {subscription}. * Let {groupedFieldSet} be the result of - {CollectFields(subscriptionType, selectionSet, null)}. + {CollectSubscriptionFields(subscriptionType, selectionSet)}. * {groupedFieldSet} must have exactly one entry, which must not be an introspection field. +CollectSubscriptionFields(objectType, selectionSet, visitedFragments): + + * If {visitedFragments} is not provided, initialize it to the empty set. + * Initialize {groupedFields} to an empty ordered map of lists. + * For each {selection} in {selectionSet}: + * {selection} must not provide the `@skip` directive. + * {selection} must not provide the `@include` directive. + * If {selection} is a {Field}: + * Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). + * Let {groupForResponseKey} be the list in {groupedFields} for + {responseKey}; if no such list exists, create it as an empty list. + * Append {selection} to the {groupForResponseKey}. + * If {selection} is a {FragmentSpread}: + * Let {fragmentSpreadName} be the name of {selection}. + * If {fragmentSpreadName} is in {visitedFragments}, continue with the + next {selection} in {selectionSet}. + * Add {fragmentSpreadName} to {visitedFragments}. + * Let {fragment} be the Fragment in the current Document whose name is + {fragmentSpreadName}. + * If no such {fragment} exists, continue with the next {selection} in + {selectionSet}. + * Let {fragmentType} be the type condition on {fragment}. + * If {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue + with the next {selection} in {selectionSet}. + * Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + * Let {fragmentGroupedFieldSet} be the result of calling + {CollectSubscriptionFields(objectType, fragmentSelectionSet, visitedFragments)}. + * For each {fragmentGroup} in {fragmentGroupedFieldSet}: + * Let {responseKey} be the response key shared by all fields in {fragmentGroup}. + * Let {groupForResponseKey} be the list in {groupedFields} for + {responseKey}; if no such list exists, create it as an empty list. + * Append all items in {fragmentGroup} to {groupForResponseKey}. + * If {selection} is an {InlineFragment}: + * Let {fragmentType} be the type condition on {selection}. + * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue + with the next {selection} in {selectionSet}. + * Let {fragmentSelectionSet} be the top-level selection set of {selection}. + * Let {fragmentGroupedFieldSet} be the result of calling {CollectSubscriptionFields(objectType, fragmentSelectionSet, visitedFragments)}. + * For each {fragmentGroup} in {fragmentGroupedFieldSet}: + * Let {responseKey} be the response key shared by all fields in {fragmentGroup}. + * Let {groupForResponseKey} be the list in {groupedFields} for + {responseKey}; if no such list exists, create it as an empty list. + * Append all items in {fragmentGroup} to {groupForResponseKey}. + * Return {groupedFields}. + +Note: This algorithm is very similar to {CollectFields()}, it differs in that +it does not have access to runtime variables and thus the `@skip` and +`@include` directives cannot be used. + **Explanatory Text** Subscription operations must have exactly one root field. To enable us to determine this without access to runtime variables, we must -forbid the `@skip` and `@include` directives in the root selection set (by -passing `null` as the `variableValues` argument to {CollectFields()}). +forbid the `@skip` and `@include` directives in the root selection set. Valid examples: diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ed62666e2..ace837f2e 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -479,28 +479,17 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -When {CollectFields()} is used during validation (see for example the -[single root field](#sec-Single-root-field) subscription operation validation -rule), the runtime value for {variableValues} will not be available - in this -case we set {variableValues} to {null} and forbid the use of the `@skip` and -`@include` directives. During execution, {variableValues} will always be -non-null. - CollectFields(objectType, selectionSet, variableValues, visitedFragments): * If {visitedFragments} is not provided, initialize it to the empty set. * Initialize {groupedFields} to an empty ordered map of lists. * For each {selection} in {selectionSet}: - * If {variableValues} is {null}: - * {selection} must not provide the `@skip` directive. - * {selection} must not provide the `@include` directive. - * Otherwise: - * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. - * If {skipDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. - * If {selection} provides the directive `@include`, let {includeDirective} be that directive. - * If {includeDirective}'s {if} argument is not {true} and is not a variable in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. + * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. + * If {skipDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}, continue with the next + {selection} in {selectionSet}. + * If {selection} provides the directive `@include`, let {includeDirective} be that directive. + * If {includeDirective}'s {if} argument is not {true} and is not a variable in {variableValues} with the value {true}, continue with the next + {selection} in {selectionSet}. * If {selection} is a {Field}: * Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). * Let {groupForResponseKey} be the list in {groupedFields} for From c22c75d9fb60938cf370eb4de718ccbaf0847ab9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Feb 2025 15:27:25 +0000 Subject: [PATCH 3/3] Fix algorithm format --- spec/Section 5 -- Validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index c13101766..2cb40c379 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -286,7 +286,7 @@ CollectSubscriptionFields(objectType, selectionSet, visitedFragments): - If no such {fragment} exists, continue with the next {selection} in {selectionSet}. - Let {fragmentType} be the type condition on {fragment}. - - If {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue + - If {DoesFragmentTypeApply(objectType, fragmentType)} is {false}, continue with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. - Let {fragmentGroupedFieldSet} be the result of calling @@ -301,7 +301,7 @@ CollectSubscriptionFields(objectType, selectionSet, visitedFragments): - If {selection} is an {InlineFragment}: - Let {fragmentType} be the type condition on {selection}. - If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, - fragmentType)} is false, continue with the next {selection} in + fragmentType)} is {false}, continue with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {selection}. - Let {fragmentGroupedFieldSet} be the result of calling