-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Spec Bug...? ProvidedRequiredArgumentsRule fails to evaluate all potential runtime types #1121
Comments
See graphql/graphql-js#4272 for a test that demonstrates the current reference implementation behavior. |
TIL! So given this schema: interface Displayable {
thumbnailUrl(size: Size! = MEDIUM): String!
}
type Album implements Displayable {
thumbnailUrl(size: Size! = LARGE): String!
}
type Query {
displayable: Displayable
} and this query: {
displayable {
thumbnailUrl
}
} I will get the LARGE thumbnail always? Because there is only 1 concrete type and the interface default value is never used? I find this surprising. The fact that the actual argument and the default value displayed by tooling such as graphiql, etc.. don't match feels pretty weird. |
If we go with (B) for (1), the existence of a default value on an interface would be meaningful, because it would result in enforcement that implementing types also have default values, but the actual value of that default value would still not be useful, still leading to the weirdness you are noting ==> assuming that tools such as graphiql are not updated. We could extend (B) even further and require all the default values of the implementing types to be identical to the default value on the interface. This would also appear to be a breaking change for some schemas => and potentially not desirable. |
Recreation using the reference implementation below; note that the schema and operation are both valid but that the resolver itself is never executed due to the behavior of CoerceArgumentValues() - i.e. this isn't a critical bug, but is something where stronger validation may be helpful. In particular, it feels like this schema should raise an error that the implementations should have a default since the interface does. I don't think the defaults have to necessarily align, though having them not align is potentially confusing. import { buildSchema, graphqlSync, parse, print, printSchema, validate, validateSchema } from "graphql";
const schema = buildSchema(/* GraphQL */ `
enum Size { MEDIUM LARGE }
interface Displayable {
thumbnailUrl(size: Size! = MEDIUM): String!
}
type Album implements Displayable {
thumbnailUrl(size: Size!): String!
}
type Query {
displayable: Displayable
}
`);
{
console.log();
const errors = validateSchema(schema);
if (errors.length > 0) {
console.log("SCHEMA IS INVALID");
console.dir(errors);
process.exit(1);
}
console.log("SCHEMA IS VALID");
console.log(printSchema(schema));
}
const source = /* GraphQL */ `
{
displayable {
thumbnailUrl
}
}
`;
{
console.log();
const ast = parse(source);
const errors = validate(schema, ast);
if (errors.length > 0) {
console.log("OPERATION IS INVALID");
console.dir(errors);
process.exit(1);
}
console.log("OPERATION IS VALID");
console.log(print(ast));
}
const result = graphqlSync({
schema,
source,
rootValue: {
displayable() {
return {
__typename: "Album",
thumbnailUrl(_, { size }) {
return size;
},
};
},
},
});
console.log();
console.log("OPERATION RESULT");
console.log(result); Output:
|
This isn't quite an RFC yet, but it should be. |
@jjergus this might be interesting to you. |
Here are the options I see:
(Note: additional thought may need to be put into nullable arguments with a default I'm currently in favour of option (3) since it's non-breaking. The fact that the specific default value is not enforced to be identical across implementations is distasteful, but we can encourage people to ensure they're the same with a SHOULD (rather than MUST) rule. This can be enforced through tools like graphql-eslint. (1) is a breaking change because querying the field on an interface would now always require the argument to be specified, which would break existing queries that do not specify the argument here. (2) is arguably breaking because the default value used in these positions may have to change, which could change the meaning of existing queries (though they would remain valid). This is relatively minor, but it's just enough for me to favour option (3) over option (2) which was my initial favourite. (3) is non-breaking from a client perspective (the only perspective that the spec generally cares about):
I propose we move ahead with option 3. |
Option 1 may not be a breaking change from a client perspective => because we would in tandem change the validation rule to the correct form, which would make sure that an argument was included unless every possible runtime type has a default. A query which was not specifying an argument would have failed previously at runtime if defaults were not specified for all runtime types, but would validate and succeed at runtime if that indeed was the case. I think! |
Adding a new object type that implements the interface without a default would be possible and a breaking change which would be undesirable, but that’s currently the case. Options 2 and 3 fix that, which is a real advantage. |
Consider this schema: interface I {
field(int: Int! = 1): Int
}
type T implements I {
field(int: Int! = 1): Int
}
type Query {
i: I
} and this query: {
i {
field
}
} I believe both of these are currently valid? If we adopt option 1, the interface would lose the default value and become: interface I {
field(int: Int!): Int
} Without your extra "make sure that an argument was included unless every possible runtime type has a default" rule, the query would now be invalid => breaking change. (I think we agree on this.) However, your proposed rule is also not sufficient because there is nothing within the schema that would enforce this to continue to hold for types newly added to the interface. Consider that we add a new type, type U implements I {
field(int: Int!): Int
}
To make your suggested additional rule work, we'd have to make an additional schema validation rule "if any implementation of an interface has a default value for one of the interface field's arguments, then all implementations of the interface must have a default value for that field's argument"; and that's a really awkward rule - the kind of thing the interface itself should enforce, by expressing the presence of the default value there, which then moves from being option (1) to being option (3). |
It's sometimes useful to have an argument that's non-nullable but at the same time optional (clients can omit it). Option (1) would make this impossible. It is a bit weird that the only way to declare such argument on an interface is to give it a meaningless default value (it doesn't matter what the default value is, just that it's there), but I don't know any better option. |
Could we add an option 4: Interfaces would not be allowed to specify default values for field arguments, but could be annotated with a directive that would enforce that all implementing types provide default values. This would avoid having the actual value of the default value for field arguments for interfaces not being meaningful… |
I think option three with the strong recommendation that they pretend it’s option 2 is fine; that way tooling can more easily enforce it. It’s also the least breaking for schema authors: people already following the option 2 pattern (which I imagine would be most people who have an interface field like this, otherwise we would have heard about it sooner?) wouldn’t need to do anything. I think most people using SDL copy/paste the interface fields into the implementation; and code first people probably already have this enforced via their programming language. |
My vote goes to option 2., anything else would be very suprising IMO:
It's breaking but as @benjie said, the fact that we're only hearing about this now is signal that the blast radius of the breaking change should be relatively small IMO.
Linking #533 about allowing to omit inherited fields from implementations. That would alleviate some of the possible pain in having to copy/paste fields. But also having to keep default values in sync. |
Just to be clear: option 2 could change the meaning of existing queries, option 3 would not. I think option 3 with a "SHOULD" for having the values match is sufficient to make it non-breaking whilst also encouraging pretty much everyone to experience option 2 from now on. |
@benjie fair enough 👍 . Always the usual Did you have anything in mind already how we can encourage the users? Warnings while assembling the schema maybe? Or linters? |
Honestly it depends how much we want to encourage it; but right now I'm thinking we forbid it with a schema lint rule in GraphQL.js and then let you pass a flag to disable that rule (something along the lines of
|
Oh so it'd be opt-out, I like that 👍 |
Consider the following schema:
As mentioned by @vepanimas at graphql/graphql-js#3214 => types implementing interfaces have to conform to their types, but not to their default values, and so the above is now considered valid.
Consider the following operation:
The ProvidedRequiredArgumentsRule as specified and implemented within the reference evaluation states that an argument for
echo
onSomeInterface
is not required, because a default value exists, but at runtime, the execution portion of the spec and the implementation take default values from the concrete runtime type, ignoring the interface. The operation will validate successfully, but throw a runtime error.Thoughts:
I think option (B) is a breaking change in terms of 1, because some schemas will break.
I think fixing the validation rule via option (B) or (Z) may also be a breaking change in terms of 2, because some operations will break => but does it count if it is a bug fix? Seems like it does, similar to #1059
The text was updated successfully, but these errors were encountered: