Skip to content
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

Closed
wants to merge 15 commits into from

Conversation

OlegIlyenko
Copy link
Contributor

This in an implementation for a spec proposal:

I think I have covered all places:

  • SDL AST & parsing
  • Schema printer (based on AST as well as based GraphQL* types)
  • Introspection
  • buildASTSchema & buildClientSchema
  • The actual validation rule

Please let me know if I forgot something.

# 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
@IvanGoncharov IvanGoncharov added this to the v14.1.0 milestone Oct 3, 2018
@@ -48,7 +70,11 @@ export function UniqueDirectivesPerLocation(
]),
);
} else {
knownDirectives[directiveName] = directive;
const repeatable = repeatableMap[directiveName];
Copy link
Member

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;
  }
  // ...
}

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, done ae9e7f9


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
Copy link
Member

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.

Copy link
Contributor Author

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].

Copy link
Member

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

Copy link
Contributor Author

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 ==.

@IvanGoncharov
Copy link
Member

Please let me know if I forgot something.

@OlegIlyenko You also need to handle repeatable in extendSchema:

return new GraphQLDirective({
name: directive.name,
description: directive.description,
locations: directive.locations,
args: extendArgs(directive.args),
astNode: directive.astNode,
});

Also remove of repeatable is breaking change so would be great to have it here:
...findRemovedDirectiveLocations(oldSchema, newSchema),

knownDirectives[directiveName] = directive;
const repeatable = repeatableMap[directiveName];

if (repeatable === undefined || !repeatable) {
Copy link
Member

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.

Copy link
Contributor Author

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.

@OlegIlyenko
Copy link
Contributor Author

@IvanGoncharov Thanks for pointing out 2 remaining places! I believe I addressed all review comments now.

@@ -33,6 +33,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
args {
...InputValue
}
repeatable
Copy link
Member

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:

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

IvanGoncharov added a commit to IvanGoncharov/graphql-js that referenced this pull request Oct 4, 2018
based on 'expectOptionalKeyword' from graphql#1541
@@ -273,4 +274,5 @@ export type IntrospectionDirective = {|
+description?: ?string,
+locations: $ReadOnlyArray<DirectiveLocationEnum>,
+args: $ReadOnlyArray<IntrospectionInputValue>,
+repeatable: boolean,
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Contributor

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.

@IvanGoncharov
Copy link
Member

@OlegIlyenko I reviewed it one more time and found only one small issue.
Rule description should be adjusted here:

/**
* Unique directive names per location
*
* A GraphQL document is only valid if all directives at a given location
* are uniquely named.
*/

@OlegIlyenko
Copy link
Contributor Author

@IvanGoncharov I updated the comment 0dff21f Thanks a lot for a fast and careful review! 🙇‍♂️

@@ -508,6 +508,7 @@ export type DirectiveDefinitionNode = {
+description?: StringValueNode,
+name: NameNode,
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>,
+isRepeatable: boolean,
Copy link
Contributor

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).

Copy link
Contributor Author

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?

Copy link
Member

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:

+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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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,
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Member

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
Copy link
Contributor

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?

Copy link
Contributor Author

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)

Copy link
Member

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.

Copy link
Contributor

@mjmahone mjmahone left a 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.

@IvanGoncharov IvanGoncharov removed this from the v14.1.0 milestone Jan 16, 2019
@IvanGoncharov IvanGoncharov added the spec RFC Implementation of a proposed change to the GraphQL specification label Feb 5, 2019
@a-type
Copy link

a-type commented May 20, 2019

Any movement on this? While it's not a blocker, it would improve syntax for a project I'm experimenting with.

@IvanGoncharov
Copy link
Member

@a-type It's blocked by introspection changes that I plan to do this week:
https://docs.google.com/presentation/d/1ISS5QhF6D2ncD8ZGChoCv1LnOWj9jryZrJ2dG11i4Mc/edit?usp=sharing

IvanGoncharov added a commit to IvanGoncharov/graphql-js that referenced this pull request Jun 9, 2019
Code is based on graphql#1541 but without introspection changes
and without breaking change detection
IvanGoncharov added a commit that referenced this pull request Jun 9, 2019
Code is based on #1541 but without introspection changes
and without breaking change detection
@IvanGoncharov
Copy link
Member

@a-type I decided that we shouldn't fully block repeatable directives because of required introspection changes so I merged everything except blocked parts as #1965.

IvanGoncharov added a commit to IvanGoncharov/graphql-js that referenced this pull request Jan 30, 2020
IvanGoncharov added a commit to IvanGoncharov/graphql-js that referenced this pull request Jan 30, 2020
IvanGoncharov added a commit to IvanGoncharov/graphql-js that referenced this pull request Jan 30, 2020
IvanGoncharov added a commit to IvanGoncharov/graphql-js that referenced this pull request Jan 30, 2020
IvanGoncharov added a commit that referenced this pull request Jan 30, 2020
@spawnia
Copy link
Member

spawnia commented Apr 13, 2020

@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.

@IvanGoncharov
Copy link
Member

@spawnia Thanks for the ping 👍
Everything is already implemented in 15.0.0 release.
Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed spec RFC Implementation of a proposed change to the GraphQL specification
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants