-
Notifications
You must be signed in to change notification settings - Fork 2k
Fragment variables: enforce fragment variable validation #1422
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
Changes from 5 commits
0d9951e
cbd3abf
e9a7991
0e1c6de
618349e
90408dc
f8be135
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -13,6 +13,7 @@ import { visit, visitWithTypeInfo } from '../language/visitor'; | |||||||||||
import { Kind } from '../language/kinds'; | ||||||||||||
import type { | ||||||||||||
DocumentNode, | ||||||||||||
ExecutableDefinitionNode, | ||||||||||||
OperationDefinitionNode, | ||||||||||||
VariableNode, | ||||||||||||
SelectionSetNode, | ||||||||||||
|
@@ -50,12 +51,12 @@ export default class ValidationContext { | |||||||||||
_fragments: ObjMap<FragmentDefinitionNode>; | ||||||||||||
_fragmentSpreads: Map<SelectionSetNode, $ReadOnlyArray<FragmentSpreadNode>>; | ||||||||||||
_recursivelyReferencedFragments: Map< | ||||||||||||
OperationDefinitionNode, | ||||||||||||
ExecutableDefinitionNode, | ||||||||||||
$ReadOnlyArray<FragmentDefinitionNode>, | ||||||||||||
>; | ||||||||||||
_variableUsages: Map<NodeWithSelectionSet, $ReadOnlyArray<VariableUsage>>; | ||||||||||||
_recursiveVariableUsages: Map< | ||||||||||||
OperationDefinitionNode, | ||||||||||||
ExecutableDefinitionNode, | ||||||||||||
$ReadOnlyArray<VariableUsage>, | ||||||||||||
>; | ||||||||||||
|
||||||||||||
|
@@ -129,14 +130,21 @@ export default class ValidationContext { | |||||||||||
return spreads; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/* | ||||||||||||
* Finds all fragments referenced via the definition, recursively. | ||||||||||||
* | ||||||||||||
* NOTE: if experimentalFragmentVariables are being used, it excludes all | ||||||||||||
* fragments with their own variable definitions: these are considered their | ||||||||||||
* own "root" executable definition. | ||||||||||||
*/ | ||||||||||||
getRecursivelyReferencedFragments( | ||||||||||||
operation: OperationDefinitionNode, | ||||||||||||
definition: ExecutableDefinitionNode, | ||||||||||||
): $ReadOnlyArray<FragmentDefinitionNode> { | ||||||||||||
let fragments = this._recursivelyReferencedFragments.get(operation); | ||||||||||||
let fragments = this._recursivelyReferencedFragments.get(definition); | ||||||||||||
if (!fragments) { | ||||||||||||
fragments = []; | ||||||||||||
const collectedNames = Object.create(null); | ||||||||||||
const nodesToVisit: Array<SelectionSetNode> = [operation.selectionSet]; | ||||||||||||
const nodesToVisit: Array<SelectionSetNode> = [definition.selectionSet]; | ||||||||||||
while (nodesToVisit.length !== 0) { | ||||||||||||
const node = nodesToVisit.pop(); | ||||||||||||
const spreads = this.getFragmentSpreads(node); | ||||||||||||
|
@@ -145,14 +153,17 @@ export default class ValidationContext { | |||||||||||
if (collectedNames[fragName] !== true) { | ||||||||||||
collectedNames[fragName] = true; | ||||||||||||
const fragment = this.getFragment(fragName); | ||||||||||||
if (fragment) { | ||||||||||||
if ( | ||||||||||||
fragment && | ||||||||||||
!isExperimentalFragmentWithVariableDefinitions(fragment) | ||||||||||||
) { | ||||||||||||
fragments.push(fragment); | ||||||||||||
nodesToVisit.push(fragment.selectionSet); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
this._recursivelyReferencedFragments.set(operation, fragments); | ||||||||||||
this._recursivelyReferencedFragments.set(definition, fragments); | ||||||||||||
} | ||||||||||||
return fragments; | ||||||||||||
} | ||||||||||||
|
@@ -181,20 +192,27 @@ export default class ValidationContext { | |||||||||||
return usages; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/* | ||||||||||||
* Finds all variables used by the definition, recursively. | ||||||||||||
* | ||||||||||||
* NOTE: if experimentalFragmentVariables are being used, it excludes all | ||||||||||||
* fragments with their own variable definitions: these are considered their | ||||||||||||
* own independent executable definition for the purposes of variable usage. | ||||||||||||
*/ | ||||||||||||
getRecursiveVariableUsages( | ||||||||||||
operation: OperationDefinitionNode, | ||||||||||||
definition: ExecutableDefinitionNode, | ||||||||||||
): $ReadOnlyArray<VariableUsage> { | ||||||||||||
let usages = this._recursiveVariableUsages.get(operation); | ||||||||||||
let usages = this._recursiveVariableUsages.get(definition); | ||||||||||||
if (!usages) { | ||||||||||||
usages = this.getVariableUsages(operation); | ||||||||||||
const fragments = this.getRecursivelyReferencedFragments(operation); | ||||||||||||
usages = this.getVariableUsages(definition); | ||||||||||||
const fragments = this.getRecursivelyReferencedFragments(definition); | ||||||||||||
for (let i = 0; i < fragments.length; i++) { | ||||||||||||
Array.prototype.push.apply( | ||||||||||||
usages, | ||||||||||||
this.getVariableUsages(fragments[i]), | ||||||||||||
); | ||||||||||||
} | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@mjmahone Idea mode 💡: I think we can have one function to do both visitReferencedFragmentsRecursively(definition, fragment => {
if (isExperimentalFragmentWithVariableDefinitions(fragment)) {
return false;
}
Array.prototype.push.apply(
usages,
this.getVariableUsages(fragments[i]),
);
}); And it will also simplify this code: graphql-js/src/validation/rules/NoUnusedFragments.js Lines 41 to 45 in e6c36e0
Plus you don't need to create intermidiate array in a process. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mjmahone Stupid idea 🤦♂️ because it will break caching. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm would it make more sense to create the ValidationContext with a flag indicating we should use fragment variables as a "validation cut" point? Then you're either opting in to it or not, and we could for instance validate "human-written" graphql with fragment variables as well as "transformed" (whatever that means) graphql in two different validation steps, just by creating a new validation context without the flag. Basically this means people using fragment variables might consume more memory/make two passes, but I'd rather have people on the experimental version have some pain than make things confusing for those on mainline GraphQL. |
||||||||||||
this._recursiveVariableUsages.set(operation, usages); | ||||||||||||
this._recursiveVariableUsages.set(definition, usages); | ||||||||||||
} | ||||||||||||
return usages; | ||||||||||||
} | ||||||||||||
|
@@ -227,3 +245,9 @@ export default class ValidationContext { | |||||||||||
return this._typeInfo.getArgument(); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
function isExperimentalFragmentWithVariableDefinitions( | ||||||||||||
ast: FragmentDefinitionNode, | ||||||||||||
): boolean %checks { | ||||||||||||
return ast.variableDefinitions != null && ast.variableDefinitions.length > 0; | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -422,12 +422,20 @@ export const testSchema = new GraphQLSchema({ | |
}); | ||
|
||
function expectValid(schema, rules, queryString) { | ||
const errors = validate(schema, parse(queryString), rules); | ||
const errors = validate( | ||
schema, | ||
parse(queryString, { experimentalFragmentVariables: true }), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are maintainers of GraphQL 3rd-party implementations that use |
||
rules, | ||
); | ||
expect(errors).to.deep.equal([], 'Should validate'); | ||
} | ||
|
||
function expectInvalid(schema, rules, queryString, expectedErrors) { | ||
const errors = validate(schema, parse(queryString), rules); | ||
const errors = validate( | ||
schema, | ||
parse(queryString, { experimentalFragmentVariables: true }), | ||
rules, | ||
); | ||
expect(errors).to.have.length.of.at.least(1, 'Should not validate'); | ||
expect(errors).to.deep.equal(expectedErrors); | ||
return errors; | ||
|
Uh oh!
There was an error while loading. Please reload this page.
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'm very concern about changes in this function since it's called
getRecursivelyReferencedFragments
but not return all recursive fragments and is part of public API.+ it breaks
NoUnusedFragments
An alternative solution would be to extract
visitReferencedFragmentsRecursively
that accept visitor function and if you returnfalse
it doesn't look inside the current fragment.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.
It only breaks
NoUnusedFragments
if you're using fragments with variable definitions. I think this is OK, as fragments with variable definitions are their own independent "unit": they have semantic meaning that is very different from normal fragments without variable definitions. An unused fragment that has variable definitions I see as being "stand-alone" in the same way a query is "stand-alone": you could reasonably figure out a way to persist these to the server and execute them, provided you started on the correct type.is, and in my mind should be, identical to
I'm very OK with breaking people who are actually using fragments with variable definitions.
I can see the appeal to extracting it out, but I'd much prefer taking an opinionated stance about what a fragment with variable definitions is. It will also make it easier to make an RFC in the spec for them.
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.
Ok, in that case, it's outside of scope for
graphql-js
and would make sense to discuss it in PR/RFC.Just a quick note:
There is no one to one matching between root types and operation types so you can write:
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 after thinking this through I think it does make more sense to add a second function specifically for excluding fragments with variable definitions. I'll make that change.