-
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] Added support for repeatable directives #1541
Conversation
# Conflicts: # src/language/__tests__/schema-kitchen-sink.graphql # src/language/__tests__/schema-printer-test.js # src/language/ast.js # src/language/printer.js # src/type/directives.js # src/type/introspection.js # src/utilities/__tests__/schemaPrinter-test.js # src/validation/rules/KnownDirectives.js # src/validation/validate.js
@@ -48,7 +70,11 @@ export function UniqueDirectivesPerLocation( | |||
]), | |||
); | |||
} else { | |||
knownDirectives[directiveName] = directive; | |||
const repeatable = repeatableMap[directiveName]; |
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.
Can you just do:
for (const directive of directives) {
const directiveName = directive.name.value;
if (repeatableMap[directiveName]) {
continue;
}
// ...
}
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 guess I generally trying to avoid usage of break
and continue
, but I can update the code and use it here.
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.
@OlegIlyenko Thinking about this can you change repeatableMap
to uniqueDirectiveMap
with reverse value and do:
for (const directive of directives) {
const directiveName = directive.name.value;
if (uniqueDirectiveMap[directiveName]) {
// ...
}
}
Plus uniqueDirectiveMap
match nicely with UniqueDirectivesPerLocation
rule name.
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.
good idea, done ae9e7f9
src/type/directives.js
Outdated
|
||
constructor(config: GraphQLDirectiveConfig): void { | ||
this.name = config.name; | ||
this.description = config.description; | ||
this.locations = config.locations; | ||
this.astNode = config.astNode; | ||
this.repeatable = | ||
config.repeatable === undefined || config.repeatable === null |
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.
In JS you can do that with a single config.repeatable == null
and we already using it in couple places.
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 believe config.repeatable === undefined
is still necessary since I defined the flow type like this:
repeatable?: ?boolean,
If I don't do the check, I will get following flow error:
Cannot assign `config.repeatable === null ? false : config.repeatable` to `this.repeatable` because:
- null or undefined [1] is incompatible with boolean [2].
- undefined [1] is incompatible with boolean [2].
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.
@OlegIlyenko Note == null
(two equal signs) compares both to null
and undefined
and Flow should handle it without problems.
If I don't do the check, I will get following flow error:
It's because you use ===
instead of ==
.
It's safe and very common pattern in JS to do == null
:
https://eslint.org/docs/rules/eqeqeq#smart
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.
Ah, I see. Haven't noticed this, thanks for the clarification.
I got an impression that the use of ==
is discouraged in javascript community since it relies of a set of coercion rules that many people find error-prone. This is why I tried to avoid using it. I was not aware of the exceptions described in the eslint documentation though. I will update the code and use ==
.
@OlegIlyenko You also need to handle graphql-js/src/utilities/extendSchema.js Lines 317 to 323 in 94b3844
Also remove of repeatable is breaking change so would be great to have it here:
|
knownDirectives[directiveName] = directive; | ||
const repeatable = repeatableMap[directiveName]; | ||
|
||
if (repeatable === undefined || !repeatable) { |
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.
Up to discussion: I don't think unknown directives should be treated as non-repeatable. Unknown directives should trigger KnownDirectives
and other rules shouldn't make any assumption about it.
Previously it was safe since all directives were non-repeatable.
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 is a good point. After thinking about it, I think I would agree with you. In both scenations: directive is not known and it is known but repeatable, it should be excluded from this validation entirely. I will adjust the logic.
@IvanGoncharov Thanks for pointing out 2 remaining places! I believe I addressed all review comments now. |
src/utilities/introspectionQuery.js
Outdated
@@ -33,6 +33,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { | |||
args { | |||
...InputValue | |||
} | |||
repeatable |
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.
@OlegIlyenko As discussed on the working group we can't assume that legacy servers would support repeatable
. So this change will break GraphiQL and many other tools.
I would suggest making option directiveRepeatable
similar to this one:
graphql-js/src/utilities/introspectionQuery.js
Lines 12 to 16 in f8378d0
export type IntrospectionOptions = {| | |
// Whether to include descriptions in the introspection result. | |
// Default: true | |
descriptions: boolean, | |
|}; |
Long term solution would be to implement two-stage introspection as suggested by Lee.
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's a good point, I added an option for it 3b5bf03 I also made if disabled by defult in order to maintain backwards compatibility.
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.
Also this one: 3a7fafa I forgot to update the introspection flow type
based on 'expectOptionalKeyword' from graphql#1541
src/utilities/introspectionQuery.js
Outdated
@@ -273,4 +274,5 @@ export type IntrospectionDirective = {| | |||
+description?: ?string, | |||
+locations: $ReadOnlyArray<DirectiveLocationEnum>, | |||
+args: $ReadOnlyArray<IntrospectionInputValue>, | |||
+repeatable: boolean, |
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.
@OlegIlyenko Can you please address this comment in spec RFC:
https://github.com/facebook/graphql/pull/472/files/1ba4e196aa710a10eceaa346d02a970dfd0b2d3a#diff-35fb422eded6e8f4df638c0d159008f6
It blocks this PR but I think we should discuss it in the scope of spec RFC.
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.
Could you please describe in more detail what kind of information you would like to add? I'm not 100% sure I understood your comment.
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.
@OlegIlyenko I mean if merge this PR that will expose repeatable
through introspection and it would be significantly harder to rename it later.
As I wrote in my comment to the spec RFC I think this field should be named isRepeatable
for consistency with isDeprecated
.
Everything else in this PR looks good so would be great to discuss this last point but since this PR is just an implementation of spec RFC I think we should discuss it in spec PR.
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 see, thanks for the clarification. Sure thing, I will open a discussion on this topic in the RFC PR.
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.
@OlegIlyenko I feel very stupid because in the first comment of this thread I linked review comment I forget to submit (review comments UI is very weird).
I already submitted it in.
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 see, thanks! now it's clear, i was confused about wrong link
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 did the renaming 56fee7d I will adjust the spec text accordingly
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.
Oh I just saw this comment. Sorry for the late comment: I guess keeping consistent with isDeprecated
makes sense, though it still feels a little weird. Oh well.
@OlegIlyenko I reviewed it one more time and found only one small issue. graphql-js/src/validation/rules/UniqueDirectivesPerLocation.js Lines 27 to 32 in 56fee7d
|
@IvanGoncharov I updated the comment 0dff21f Thanks a lot for a fast and careful review! 🙇♂️ |
src/language/ast.js
Outdated
@@ -508,6 +508,7 @@ export type DirectiveDefinitionNode = { | |||
+description?: StringValueNode, | |||
+name: NameNode, | |||
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>, | |||
+isRepeatable: boolean, |
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 wonder if this should be:
+repeatable?: boolean,
If someone never uses repeatable directives, this adds no extra weight to the node.
I also don't think we need the is
prefix: the fact that it's a boolean is enough information, and the question repeatable? false
is close-to-valid English as-is (just as understandable as is repeatable? false
).
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 is
prefix primarily added for consistency with isDeprecated
, as @IvanGoncharov pointed out https://github.com/facebook/graphql/pull/472/files/1ba4e196aa710a10eceaa346d02a970dfd0b2d3a#diff-35fb422eded6e8f4df638c0d159008f6
I think I would prefer it keep it mandatory. This makes it easier to work with and i feel that the performance overhead is negligible. But I can also make it optional.
WDYT?
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.
@OlegIlyenko In the context of AST, I'm more in favor of @mjmahone proposal since we already have block
with similar prototype:
graphql-js/src/language/ast.js
Line 320 in 0dff21f
+block?: boolean, |
Plus it looks like JS AST doesn't use is
prefix, e.g. async
:
https://github.com/estree/estree/blob/df2290b23a652e1ec354b6775459a3b42e22f108/es2017.md#function
But outside of AST I strongly believe it should be named isRepeatable
for the consistency with isDeprecated
.
This makes it easier to work with and i feel that the performance overhead is negligible.
I'm with you on that point especially since JS parser always preserve boolean flags and they typically work with way bigger files than GraphQL.
Also defacto (in our parser) we always provide values for all keys so why should Flow typings mark that as optional.
That said I think it's outside of scope of this PR and should be disscussed for AST as a whole and not for individual flags.
So for now I think it should be optional.
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 think both alternatives should be fine. I also considered keep repeatable
name on AST nodes, but then renamed it with is
prefix across the whole codebase for the sake of the naming consistency.
But I think it should totally fine to have repeatable
field on AST node and then use isRepeatable
everywhere else. @mjmahone What do you think?
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 renamed isRepeatable
back to repeatable
in all places except introspection: 89f5780
* are uniquely named. | ||
*/ | ||
export function UniqueDirectivesPerLocation( | ||
context: ASTValidationContext, | ||
context: ValidationContext | SDLValidationContext, |
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 technically a breaking change. It's probably OK as I doubt anyone is using this validation explicitly yet, but definitely need to note that in our release notes.
But I think there should be a way, today, to re-order our validation contexts to provide a Schema SDL if possible. I will probably end up forking all of these validation rules to run off an AST-only schema object, because building (and extending) the current GraphQLSchema object is too slow. I won't block this on not using the GraphQLSchema, but I'd like for us to move in that direction.
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.
Do understand it correctly: you mean expressing all existing validations purely in terms of SDL context? Does this also imply that in case of normal query validation you would like to transform existing schema in SDL and then validate against it?
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.
@mjmahone It's not a breaking change since ASTValidationContext
is not a part of public API. As we agree during 14.0.0
SDL validation is an experimental feature so I didn't expose this and other new class as public API.
ATM only ValidationContext
is exposed so if library client use only public API he shouldn't be affected by this change in any way.
@@ -13,10 +13,15 @@ export type IntrospectionOptions = {| | |||
// Whether to include descriptions in the introspection result. | |||
// Default: true | |||
descriptions: boolean, | |||
// Whether to include `isRepeatable` flag on directives. | |||
// Default: false |
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.
Why would this default to false?
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.
For example, if it would be true
, then after an update without any adjustments third-party libraries (like GraphiQL) would start to send queries with isRepeatable
flag to the servers that potentially not updated yet. So default is primarily defined like this to maintain backwards compatibility.
Would you like to change the default and require all downstream libraries to explicitly set this flag? (at least at the initial stage where servers are not yet updated)
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.
@mjmahone Do you remember we discussed it on the last WG in the scope of my
"schema description" RFC. And the consensus was to keep it backward compatible for now but design two stage introspection as a long-term solution.
# Conflicts: # src/language/printer.js
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 looks really good now! Thanks for pushing this PR forward @OlegIlyenko, this idea is in much better shape thanks to all the work you've put into it, and I am glad it's turned out to be a fairly surgically-precise PR to enable it.
The backwards-compatibility problem with schema introspection makes me think schema introspection isn't that useful: we probably can migrate the introspection query to actually just return a string of the schema text. But that's a question we can work towards fixing later.
Any movement on this? While it's not a blocker, it would improve syntax for a project I'm experimenting with. |
@a-type It's blocked by introspection changes that I plan to do this week: |
Code is based on graphql#1541 but without introspection changes and without breaking change detection
Code is based on #1541 but without introspection changes and without breaking change detection
Code is taken from #1541
Code is taken from graphql#1541
Code is taken from #1541
@IvanGoncharov Is there anything left in this PR that you have not merged elsewhere? Asking because i am working on the PHP implementation just now. |
@spawnia Thanks for the ping 👍 |
This in an implementation for a spec proposal:
I think I have covered all places:
GraphQL*
types)buildASTSchema
&buildClientSchema
Please let me know if I forgot something.