-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[resolvers] CODEGEN-500: Support semanticNonNull custom directive #10315
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
Conversation
🦋 Changeset detectedLatest commit: 1cf1924 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🚀 Snapshot Release (
|
Package | Version | Info |
---|---|---|
@graphql-codegen/visitor-plugin-common |
5.8.0-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-document-nodes |
4.0.16-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/gql-tag-operations |
4.0.17-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-operations |
4.5.2-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-resolvers |
4.5.0-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typed-document-node |
5.1.1-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript |
4.1.6-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/client-preset |
4.7.1-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/graphql-modules-preset |
4.0.16-alpha-20250320130201-1cf1924a1da9258b15294b0251bba9dc61aa9ad4 |
npm ↗︎ unpkg ↗︎ |
💻 Website PreviewThe latest changes are available as preview in: https://pr-10315.graphql-code-generator.pages.dev |
Hi @alf, could you please try the following alpha versions:
|
const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => { | ||
const baseType = getBaseTypeNode(original.type); | ||
const realType = baseType.name.value; | ||
const typeToUse = this.getTypeToUse(realType); | ||
/** | ||
* Turns GraphQL type to TypeScript types (`mappedType`) e.g. | ||
* - String! -> ResolversTypes['String']> | ||
* - String -> Maybe<ResolversTypes['String']> | ||
* - [String] -> Maybe<Array<Maybe<ResolversTypes['String']>>> | ||
* - [String!]! -> Array<ResolversTypes['String']> | ||
*/ | ||
const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type); | ||
|
||
const subscriptionType = this._schema.getSubscriptionType(); | ||
const isSubscriptionType = subscriptionType && subscriptionType.name === parentName; | ||
|
||
if (isSubscriptionType) { | ||
return { | ||
mappedTypeKey: `${mappedType}, "${node.name}"`, | ||
resolverType: 'SubscriptionResolver', | ||
}; | ||
} | ||
|
||
const directiveMappings = | ||
node.directives | ||
?.map(directive => this._directiveResolverMappings[directive.name as any]) | ||
.filter(Boolean) | ||
.reverse() ?? []; | ||
|
||
const resolverType = isSubscriptionType ? 'SubscriptionResolver' : directiveMappings[0] ?? 'Resolver'; | ||
return { | ||
mappedTypeKey: this.modifyFieldDefinitionNodeTransformedType({ node: original, originalType: mappedType }), | ||
resolverType: directiveMappings[0] ?? 'Resolver', | ||
}; | ||
})(); |
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 refactored this bit to bring related variables together, in the same scope - so it's easier to read
FWIW, the type Query {
a: [String] @semanticNonNull # List<String?>
b: [String] @semanticNonNull(levels: [1]) # List<String>?
c: [String] @semanticNonNull(levels: [0,1]) # List<String>
} |
Using TypeScript syntax:
You could also use |
It's also important to note that you should not use a non-nullable type for semantic non-nullable unless the client is an error handling client; i.e. the client must either:
To put it another way: users of graphql-codegen must opt-in to these positions being non-nullable for them; if they don't opt-in then the directive should be ignored and the default nullable should be carried through. |
Thanks @martinbonnin @BenjieGillam for helping me understand this directive!
Did you mean something like this? type Foo {
bar: String! @semanticNonNull
}
This sounds like a behaviour for the client part of codegen if I understand it correctly? If so, I'll keep that in mind when implementing the client part |
I meant that type Query {
bar: String @semanticNonNull
} With query query Q {
bar
} Should come out as: type Q = {
bar: string | null
} unless the user opts in to having it come out as non-nullable (e.g. via a configuration parameter, choice of preset, etc), at which point you'd generate: type Q = {
bar: string
} This is because query {
"data": { "bar": null },
"errors": [{"message": "Blah blah blah", "path": ["bar"]}]
} e.g. strictly the field is still If you're doing pure type generation then you have no control over this. If you're doing code generation too, then you can - you can choose to throw the errors either when the user reads from the field that's broken (e.g. using a "getter" as we do in Personally I'd recommend using graphql-toe, it ensures that no matter how you read the data (once it has gone through TOE) whenever you read an errored field, an error will be thrown. I also put quite a bit of effort into ensuring it was as efficient as possible, only creating new types/adding getters where it had to, and carrying through the source data otherwise. |
type Q = {
bar: string | null
}
Disagree on this one. If someone takes the time to annotate their schema with If the client is not an error handling client, I'm fine with an option to ignore |
Strong disagree; the team producing the schema (e.g. adding If we were to generate non-nulls from semantic non-null right now, then the types would be a lie - e.g. someone using a semantic non-null marked schema with Apollo Client would be able to read If it's the client team adding |
@eddeee888 I've just pushed out a new version of import { semanticToStrict, semanticToNullable } from 'graphql-sock';
//...
const schema =
codegenOptions.errorHandlingClient
? semanticToStrict(sourceSchema)
: semanticToNullable(sourceSchema); Note that graphql-sock supports both the |
Hi @benjie, Some request I have with this current version:
|
Good catch; I forgot that was why I only had CLI docs and not library docs! Should be feasible for it to |
@eddeee888 I've released Oh, also it now exports a |
testingSchema, | ||
[], | ||
{ | ||
customDirectives: { semanticNonNull: true }, |
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.
Note that semanticNonNull is not necessarily a directive - it can also be represented by syntax (and because you're using graphql-sock the syntax should automatically work).
Further, fields can be marked as semantically non-nullable in the schema without the client having any handling code, in which case the user can still read null
from those positions (on error), and thus the generated code saying it's non-nullable would be a lie.
Instead, consider renaming the option to something that indicates that the client is handling errors such that you can never read a null
from a position that is linked to an error:
customDirectives: { semanticNonNull: true }, | |
noErrorNulls: true, |
Essentially:
- if you use
graphql-toe
or similar then you can setnoErrorNulls: true
and GraphQL Codegen can output non-nullable in positions that are semantically non-nullable - if you don't have any specific client handling code, then Codegen should output these same positions as nullable, since they will be null if an error occurs
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.
Since this is the PR for typescript-resolvers
, we should look at this from the server's POV.
Note that semanticNonNull is not necessarily a directive - it can also be represented by syntax (and because you're using graphql-sock the syntax should automatically work).
I feel there could a future where the syntax can be handled out-of-the-box by codegen i.e. it is possible for codegen to treat the semanticNonNull syntax (*
in option 1 IIRC?) the same as !
. When that happens, users can stop using this config option.
Further, fields can be marked as semantically non-nullable in the schema without the client having any handling code, in which case the user can still read null from those positions (on error), and thus the generated code saying it's non-nullable would be a lie.
In this PR, the types are for resolver return types. In my mind, these resolvers will get TypeScript error when returning null. They must throw for the value to be null, in which case clients should handle accordingly.
Unless I misunderstood the server's responsibilities when handling semanticNonNull? 🤔
noErrorNulls: true,
I feel something like noErrorNulls
on clients makes sense, and each client may choose to handle this differently. However, the server shouldn't change how it returns non-nullable data, and throw error to force null data.
Again, I could be misunderstanding the server's role here. Keen to hear advices if so 🙂
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.
Nope this is entirely my mistake; I missed this was for the server side and thought it was client side. Server side, semantic non-null and traditional non-null are equivalent: a null must not be returned. I personally wouldn’t have a setting server-side, just always enable it if either the @semanticNonNull directive is used or the syntax is used (i.e. just feed the source schema through semanticToStrict unconditionally); but you should do whatever is right for the library.
Related CODEGEN-500
Description
Based on #10159
Related to #10151
When
@semanticNonNull
custom directive is applied to a nullable field:Type of change
How Has This Been Tested?